跳轉到內容

外掛 API

Vite 外掛擴充套件了 Rolldown 的外掛介面,並提供了一些額外的 Vite 特有選項。因此,您可以編寫一次 Vite 外掛,並使其同時適用於開發環境和生產構建。

建議在閱讀以下部分之前,先通讀 Rolldown 的外掛文件

編寫外掛

Vite 致力於開箱即用地提供成熟的模式,因此在建立新外掛之前,請務必檢視 功能指南,看看您的需求是否已涵蓋。同時,請查閱現有的社群外掛,包括 相容的 Rollup 外掛Vite 專用外掛

在建立外掛時,您可以將其內聯在 vite.config.js 中,無需為其建立新包。一旦您發現該外掛在專案中非常有用,可以考慮分享它以幫助 生態系統中的其他人

提示

在學習、除錯或編寫外掛時,我們建議在專案中包含 vite-plugin-inspect。它允許您檢查 Vite 外掛的中間狀態。安裝後,您可以訪問 localhost:5173/__inspect/ 來檢視專案的模組和轉換堆疊。請檢視 vite-plugin-inspect 文件 中的安裝說明。 vite-plugin-inspect

約定

如果外掛不使用 Vite 特有的鉤子,並且可以實現為 相容的 Rolldown 外掛,那麼建議使用 Rolldown 外掛命名約定

  • Rolldown 外掛應具有清晰的名稱,並以 rolldown-plugin- 為字首。
  • 在 package.json 的 keywords 欄位中包含 rolldown-pluginvite-plugin 關鍵字。

這樣可以使該外掛也能在純 Rolldown 或基於 Rollup 的專案中使用。

對於僅限 Vite 使用的外掛

  • Vite 外掛應具有清晰的名稱,並以 vite-plugin- 為字首。
  • 在 package.json 的 keywords 欄位中包含 vite-plugin 關鍵字。
  • 在外掛文件中包含一個章節,詳細說明為什麼它是一個僅限 Vite 的外掛(例如,它使用了 Vite 特有的外掛鉤子)。

如果您的外掛僅適用於特定框架,則應將框架名稱作為字首的一部分。

  • Vue 外掛使用 vite-plugin-vue- 字首
  • React 外掛使用 vite-plugin-react- 字首
  • Svelte 外掛使用 vite-plugin-svelte- 字首

另請參閱 虛擬模組約定

外掛配置

使用者會將外掛新增到專案的 devDependencies 中,並使用 plugins 陣列選項進行配置。

vite.config.js
js
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'

export default defineConfig({
  plugins: [vitePlugin(), rollupPlugin()],
})

假值(falsy)的外掛將被忽略,這可用於輕鬆啟用或停用外掛。

plugins 也接受包含多個外掛作為單個元素的預設。這對於實現複雜功能(如框架整合)非常有用,這些功能通常由多個外掛組成。該陣列在內部會被扁平化處理。

js
// framework-plugin
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'

export default function framework(config) {
  return [frameworkRefresh(config), frameworkDevTools(config)]
}
vite.config.js
js
import { defineConfig } from 'vite'
import framework from 'vite-plugin-framework'

export default defineConfig({
  plugins: [framework()],
})

簡單示例

提示

通常,將 Vite/Rolldown/Rollup 外掛編寫為一個返回實際外掛物件的工廠函式是一種慣例。該函式可以接受選項,從而允許使用者自定義外掛的行為。

轉換自定義檔案型別

js
const fileRegex = /\.(my-file-ext)$/

export default function myPlugin() {
  return {
    name: 'transform-file',

    transform: {
      filter: {
        id: fileRegex,
      },
      handler(src, id) {
        return {
          code: compileFileToJS(src),
          map: null, // provide source map if available
        }
      },
    },
  }
}

匯入虛擬檔案

請參閱 下一節 中的示例。

虛擬模組約定

虛擬模組是一種有用的方案,允許您使用正常的 ESM 匯入語法將構建時資訊傳遞給原始檔。

js
import { exactRegex } from '@rolldown/pluginutils'

export default function myPlugin() {
  const virtualModuleId = 'virtual:my-module'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'my-plugin', // required, will show up in warnings and errors
    resolveId: {
      filter: { id: exactRegex(virtualModuleId) },
      handler() {
        return resolvedVirtualModuleId
      },
    },
    load: {
      filter: { id: exactRegex(resolvedVirtualModuleId) },
      handler() {
        return `export const msg = "from virtual module"`
      },
    },
  }
}

這允許在 JavaScript 中匯入該模組

js
import { msg } from 'virtual:my-module'

console.log(msg)

按照約定,Vite(以及 Rolldown / Rollup)中的虛擬模組在面向使用者的路徑前帶有 virtual: 字首。如果可能,應將外掛名稱用作名稱空間,以避免與生態系統中的其他外掛衝突。例如,一個 vite-plugin-posts 外掛可以要求使用者匯入 virtual:postsvirtual:posts/helpers 虛擬模組以獲取構建時資訊。在內部,使用虛擬模組的外掛應在解析 ID 時為模組 ID 新增 \0 字首,這是 Rollup 生態系統中的一個約定。這可以防止其他外掛嘗試處理該 ID(如 node 解析),核心功能(如 sourcemaps)可以使用此資訊來區分虛擬模組和常規檔案。\0 在匯入 URL 中是不允許的字元,因此我們必須在匯入分析期間替換它們。在開發環境的瀏覽器中,一個 \0{id} 虛擬 ID 最終會被編碼為 /@id/__x00__{id}。該 ID 在進入外掛流水線之前會被解碼回原樣,因此外掛鉤子程式碼不會看到這種編碼。

請注意,直接衍生自真實檔案的模組(例如單檔案元件中的指令碼模組,如 .vue 或 .svelte SFC)不需要遵循此約定。SFC 在處理時通常會生成一組子模組,但這些子模組中的程式碼可以映射回檔案系統。對這些子模組使用 \0 會導致 sourcemaps 無法正常工作。

通用鉤子

在開發過程中,Vite 開發伺服器會建立一個外掛容器,該容器以與 Rolldown 相同的方式呼叫 Rolldown 構建鉤子

以下鉤子在伺服器啟動時呼叫一次

以下鉤子在每個傳入的模組請求時呼叫

這些鉤子還有一個擴充套件的 options 引數,包含額外的 Vite 特有屬性。您可以在 SSR 文件 中瞭解更多資訊。

某些 resolveId 呼叫中的 importer 值可能是根目錄下通用 index.html 的絕對路徑,因為由於 Vite 的非打包開發伺服器模式,並不總是能推匯出實際的 importer。對於 Vite 解析流水線內處理的匯入,可以在匯入分析階段跟蹤 importer,從而提供正確的 importer 值。

以下鉤子在伺服器關閉時呼叫

請注意,moduleParsed 鉤子在開發期間**不會**被呼叫,因為 Vite 為了更好的效能避免了完整的 AST 解析。

輸出生成鉤子closeBundle 除外)在開發期間**不會**被呼叫。

Vite 特有鉤子

Vite 外掛還可以提供具有特定於 Vite 用途的鉤子。這些鉤子會被 Rollup 忽略。

config

  • 型別: (config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void

  • 型別: async, sequential

    在解析 Vite 配置之前修改它。該鉤子接收原始使用者配置(CLI 選項與配置檔案合併後的結果)以及當前配置環境(暴露了正在使用的 modecommand)。它可以返回一個部分配置物件,該物件將與現有配置進行深度合併,或者直接修改配置(如果預設合併無法達到預期結果)。

    示例

    js
    // return partial config (recommended)
    const partialConfigPlugin = () => ({
      name: 'return-partial',
      config: () => ({
        resolve: {
          alias: {
            foo: 'bar',
          },
        },
      }),
    })
    
    // mutate the config directly (use only when merging doesn't work)
    const mutateConfigPlugin = () => ({
      name: 'mutate-config',
      config(config, { command }) {
        if (command === 'build') {
          config.root = 'foo'
        }
      },
    })

    注意

    使用者外掛在執行此鉤子之前已解析,因此在 config 鉤子中注入其他外掛將不會生效。

configResolved

  • 型別: (config: ResolvedConfig) => void | Promise<void>

  • 型別: async, parallel

    在 Vite 配置解析後呼叫。使用此鉤子讀取並存儲最終解析的配置。當外掛需要根據當前執行的命令執行不同操作時,此鉤子也非常有用。

    示例

    js
    const examplePlugin = () => {
      let config
    
      return {
        name: 'read-config',
    
        configResolved(resolvedConfig) {
          // store the resolved config
          config = resolvedConfig
        },
    
        // use stored config in other hooks
        transform(code, id) {
          if (config.command === 'serve') {
            // dev: plugin invoked by dev server
          } else {
            // build: plugin invoked by Rollup
          }
        },
      }
    }

    注意:在開發環境中,command 值為 serve(在 CLI 中 vitevite devvite serve 是別名)。

configureServer

  • 型別: (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

  • 型別: async, sequential

  • 另請參閱: ViteDevServer

    用於配置開發伺服器的鉤子。最常見的用例是向內部 connect 應用新增自定義中介軟體。

    js
    const myPlugin = () => ({
      name: 'configure-server',
      configureServer(server) {
        server.middlewares.use((req, res, next) => {
          // custom handle request...
        })
      },
    })

    注入後續中介軟體

    configureServer 鉤子在安裝內部中介軟體之前呼叫,因此自定義中介軟體預設會在內部中介軟體之前執行。如果您希望在內部中介軟體**之後**注入中介軟體,可以從 configureServer 返回一個函式,該函式將在安裝內部中介軟體後被呼叫。

    js
    const myPlugin = () => ({
      name: 'configure-server',
      configureServer(server) {
        // return a post hook that is called after internal middlewares are
        // installed
        return () => {
          server.middlewares.use((req, res, next) => {
            // custom handle request...
          })
        }
      },
    })

    儲存伺服器訪問許可權

    在某些情況下,其他外掛鉤子可能需要訪問開發伺服器例項(例如訪問 WebSocket 伺服器、檔案系統監視器或模組圖)。此鉤子也可用於儲存伺服器例項,以便在其他鉤子中訪問。

    js
    const myPlugin = () => {
      let server
      return {
        name: 'configure-server',
        configureServer(_server) {
          server = _server
        },
        transform(code, id) {
          if (server) {
            // use server...
          }
        },
      }
    }

    注意:在執行生產構建時不會呼叫 configureServer,因此您的其他鉤子需要防止其缺失的情況。

configurePreviewServer

  • 型別: (server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>

  • 型別: async, sequential

  • 另請參閱: PreviewServer

    configureServer 相同,但用於預覽伺服器。與 configureServer 類似,configurePreviewServer 鉤子在安裝其他中介軟體之前呼叫。如果您希望在其他中介軟體**之後**注入中介軟體,可以從 configurePreviewServer 返回一個函式,該函式將在安裝內部中介軟體後被呼叫。

    js
    const myPlugin = () => ({
      name: 'configure-preview-server',
      configurePreviewServer(server) {
        // return a post hook that is called after other middlewares are
        // installed
        return () => {
          server.middlewares.use((req, res, next) => {
            // custom handle request...
          })
        }
      },
    })

transformIndexHtml

  • 型別: IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }

  • 型別: async, sequential

    用於轉換 HTML 入口點檔案(如 index.html)的專用鉤子。該鉤子接收當前的 HTML 字串和一個轉換上下文。在開發期間,該上下文暴露了 ViteDevServer 例項,在構建期間暴露了 Rollup 輸出 bundle。

    該鉤子可以是非同步的,並可以返回以下內容之一:

    • 轉換後的 HTML 字串
    • 要注入到現有 HTML 中的標籤描述符物件陣列({ tag, attrs, children })。每個標籤還可以指定應注入到何處(預設為前置到 <head> 中)
    • 包含上述兩者的物件,形式為 { html, tags }

    預設情況下,orderundefined,此鉤子在 HTML 轉換後應用。為了注入一個需要透過 Vite 外掛流水線的指令碼,order: 'pre' 將在處理 HTML 之前應用該鉤子。order: 'post' 將在所有 orderundefined 的鉤子應用後應用該鉤子。

    基礎示例

    js
    const htmlPlugin = () => {
      return {
        name: 'html-transform',
        transformIndexHtml(html) {
          return html.replace(
            /<title>(.*?)<\/title>/,
            `<title>Title replaced!</title>`,
          )
        },
      }
    }

    完整鉤子簽名

    ts
    type IndexHtmlTransformHook = (
      html: string,
      ctx: {
        path: string
        filename: string
        server?: ViteDevServer
        bundle?: import('rollup').OutputBundle
        chunk?: import('rollup').OutputChunk
      },
    ) =>
      | IndexHtmlTransformResult
      | void
      | Promise<IndexHtmlTransformResult | void>
    
    type IndexHtmlTransformResult =
      | string
      | HtmlTagDescriptor[]
      | {
          html: string
          tags: HtmlTagDescriptor[]
        }
    
    interface HtmlTagDescriptor {
      tag: string
      /**
       * attribute values will be escaped automatically if needed
       */
      attrs?: Record<string, string | boolean>
      children?: string | HtmlTagDescriptor[]
      /**
       * default: 'head-prepend'
       */
      injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
    }

    注意

    如果您使用的是對入口檔案有自定義處理的框架(例如 SvelteKit),則不會呼叫此鉤子。

handleHotUpdate

  • 型別: (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>

  • 型別: async, sequential

  • 另請參閱: HMR API

    執行自定義 HMR 更新處理。該鉤子接收一個上下文物件,具有以下簽名:

    ts
    interface HmrContext {
      file: string
      timestamp: number
      modules: Array<ModuleNode>
      read: () => string | Promise<string>
      server: ViteDevServer
    }
    • modules 是一個受更改檔案影響的模組陣列。它是一個數組,因為單個檔案可能對映到多個服務模組(例如 Vue SFC)。

    • read 是一個非同步讀取函式,返回檔案內容。提供此功能是因為在某些系統上,檔案更改回調觸發得太快,編輯器還沒完成檔案更新,直接 fs.readFile 會返回空內容。傳入的讀取函式規範化了此行為。

    鉤子可以選擇:

    • 過濾並縮小受影響的模組列表,使 HMR 更加準確。

    • 返回一個空陣列並執行完全重新載入

      js
      handleHotUpdate({ server, modules, timestamp }) {
        // Invalidate modules manually
        const invalidatedModules = new Set()
        for (const mod of modules) {
          server.moduleGraph.invalidateModule(
            mod,
            invalidatedModules,
            timestamp,
            true
          )
        }
        server.ws.send({ type: 'full-reload' })
        return []
      }
    • 返回一個空陣列並透過向客戶端傳送自定義事件執行完全自定義的 HMR 處理

      js
      handleHotUpdate({ server }) {
        server.ws.send({
          type: 'custom',
          event: 'special-update',
          data: {}
        })
        return []
      }

      客戶端程式碼應使用 HMR API 註冊相應的處理程式(這可以透過同一外掛的 transform 鉤子注入)

      js
      if (import.meta.hot) {
        import.meta.hot.on('special-update', (data) => {
          // perform custom update
        })
      }

外掛上下文元資料

對於可以訪問外掛上下文的鉤子,Vite 在 this.meta 上暴露了額外的屬性:

  • this.meta.viteVersion:當前的 Vite 版本字串(例如 "8.0.0")。

檢測由 Rolldown 驅動的 Vite

this.meta.rolldownVersion 僅適用於由 Rolldown 驅動的 Vite(即 Vite 8+)。您可以使用它來檢測當前的 Vite 例項是否由 Rolldown 驅動。

ts
function versionCheckPlugin(): Plugin {
  return {
    name: 'version-check',
    buildStart() {
      if (this.meta.rolldownVersion) {
        // only do something if running on a Rolldown powered Vite
      } else {
        // do something else if running on a Rollup powered Vite
      }
    },
  }
}

輸出 Bundle 元資料

在構建過程中,Vite 會使用 Vite 特有的 viteMetadata 欄位增強 Rolldown 的構建輸出物件。

這可以透過以下方式獲取:

  • RenderedChunk(例如在 renderChunkaugmentChunkHash 中)
  • OutputChunkOutputAsset(例如在 generateBundlewriteBundle 中)

viteMetadata 提供:

  • viteMetadata.importedCss: Set<string>
  • viteMetadata.importedAssets: Set<string>

這在編寫需要檢查發出的 CSS 和靜態資源而不依賴 build.manifest 的外掛時非常有用。

示例

vite.config.ts
ts
function outputMetadataPlugin(): Plugin {
  return {
    name: 'output-metadata-plugin',
    generateBundle(_, bundle) {
      for (const output of Object.values(bundle)) {
        const css = output.viteMetadata?.importedCss
        const assets = output.viteMetadata?.importedAssets
        if (!css?.size && !assets?.size) continue

        console.log(output.fileName, {
          css: css ? [...css] : [],
          assets: assets ? [...assets] : [],
        })
      }
    },
  }
}

外掛排序

Vite 外掛可以額外指定一個 enforce 屬性(類似於 webpack 載入器)來調整其應用順序。enforce 的值可以是 "pre""post"。解析後的外掛將按以下順序排列:

  • 別名(Alias)
  • 帶有 enforce: 'pre' 的使用者外掛
  • Vite 核心外掛
  • 沒有 enforce 值的使用者外掛
  • Vite 構建外掛
  • 帶有 enforce: 'post' 的使用者外掛
  • Vite 構建後外掛(壓縮、清單、報告)

請注意,這與鉤子排序不同,鉤子仍然按照 Rolldown 鉤子通常的 order 屬性 單獨受制於其順序。

條件應用

預設情況下,外掛在服務(serve)和構建(build)時都會被呼叫。如果外掛僅需在服務或構建期間有條件地應用,請使用 apply 屬性,僅在 'build''serve' 時呼叫它們。

js
function myPlugin() {
  return {
    name: 'build-only',
    apply: 'build', // or 'serve'
  }
}

也可以使用函式進行更精確的控制。

js
apply(config, { command }) {
  // apply only on build but not for SSR
  return command === 'build' && !config.build.ssr
}

Rolldown 外掛相容性

相當一部分 Rolldown / Rollup 外掛可以直接作為 Vite 外掛工作(例如 @rollup/plugin-alias@rollup/plugin-json),但並非全部,因為某些外掛鉤子在非打包的開發伺服器上下文中沒有意義。

通常情況下,只要 Rolldown / Rollup 外掛符合以下標準,它就可以直接作為 Vite 外掛工作:

  • 它不使用 moduleParsed 鉤子。
  • 它不依賴於 Rolldown 特有的選項,如 transform.inject
  • 它在 bundle 階段鉤子和 output 階段鉤子之間沒有強耦合。

如果 Rolldown / Rollup 外掛僅對構建階段有意義,則可以將其指定在 build.rolldownOptions.plugins 下。它將作為帶有 enforce: 'post'apply: 'build' 的 Vite 外掛執行。

您還可以使用 Vite 特有的屬性增強現有的 Rolldown / Rollup 外掛。

vite.config.js
js
import example from 'rolldown-plugin-example'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      ...example(),
      enforce: 'post',
      apply: 'build',
    },
  ],
})

路徑規範化

Vite 在解析 ID 時會將路徑規範化為使用 POSIX 分隔符 ( / ),同時在 Windows 上保留卷。另一方面,Rollup 預設保持已解析路徑不變,因此已解析 ID 在 Windows 上帶有 win32 分隔符 ( \ )。不過,Rollup 外掛在內部使用來自 @rollup/pluginutilsnormalizePath 工具函式,它在執行比較之前將分隔符轉換為 POSIX。這意味著當這些外掛在 Vite 中使用時,includeexclude 配置模式以及其他針對已解析 ID 比較的類似路徑都能正常工作。

因此,對於 Vite 外掛,在比較路徑與已解析 ID 時,首先將路徑規範化為使用 POSIX 分隔符非常重要。vite 模組匯出了一個等效的 normalizePath 工具函式。

js
import { normalizePath } from 'vite'

normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'

過濾、包含/排除模式

Vite 暴露了 @rollup/pluginutilscreateFilter 函式,以鼓勵 Vite 特有外掛和整合使用標準的包含/排除過濾模式,這在 Vite 核心中也使用了該模式。

鉤子過濾器

Rolldown 引入了 鉤子過濾器功能,以減少 Rust 和 JavaScript 執行時之間的通訊開銷。此功能允許外掛指定模式來確定何時呼叫鉤子,透過避免不必要的鉤子呼叫來提高效能。

Rollup 4.38.0+ 和 Vite 6.3.0+ 也支援此功能。為了使您的外掛與舊版本向後相容,請確保同時在鉤子處理程式內部執行過濾器。

js
export default function myPlugin() {
  const jsFileRegex = /\.js$/

  return {
    name: 'my-plugin',
    // Example: only call transform for .js files
    transform: {
      filter: {
        id: jsFileRegex,
      },
      handler(code, id) {
        // Additional check for backward compatibility
        if (!jsFileRegex.test(id)) return null

        return {
          code: transformCode(code),
          map: null,
        }
      },
    },
  }
}

提示

@rolldown/pluginutils 匯出了一些用於鉤子過濾器的工具,如 exactRegexprefixRegex。為了方便起見,這些也從 rolldown/filter 中重新匯出。

客戶端-伺服器通訊

從 Vite 2.9 開始,我們為外掛提供了一些工具來幫助處理與客戶端的通訊。

伺服器到客戶端

在外掛端,我們可以使用 server.ws.send 向客戶端廣播事件。

vite.config.js
js
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('connection', () => {
          server.ws.send('my:greetings', { msg: 'hello' })
        })
      },
    },
  ],
})

注意

我們建議**始終為您的事件名稱新增字首**,以避免與其他外掛發生衝突。

在客戶端,使用 hot.on 監聽這些事件。

ts
// client side
if (import.meta.
hot
) {
import.meta.
hot
.
on
('my:greetings', (
data
) => {
console
.
log
(
data
.msg) // hello
}) }

客戶端到伺服器

要從客戶端向伺服器傳送事件,我們可以使用 hot.send

ts
// client side
if (import.meta.hot) {
  import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}

然後使用 server.ws.on 在伺服器端監聽這些事件。

vite.config.js
js
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('my:from-client', (data, client) => {
          console.log('Message from client:', data.msg) // Hey!
          // reply only to the client (if needed)
          client.send('my:ack', { msg: 'Hi! I got your message!' })
        })
      },
    },
  ],
})

自定義事件的 TypeScript

在內部,Vite 從 CustomEventMap 介面推斷 payload 的型別,可以透過擴充套件該介面來定義自定義事件型別。

注意

指定 TypeScript 宣告檔案時,請確保包含 .d.ts 副檔名。否則,TypeScript 可能不知道該模組試圖擴充套件哪個檔案。

events.d.ts
ts
import 'vite/types/customEvent.d.ts'

declare module 'vite/types/customEvent.d.ts' {
  interface CustomEventMap {
    'custom:foo': { msg: string }
    // 'event-key': payload
  }
}

此介面擴充套件被 InferCustomEventPayload<T> 用來推斷事件 T 的 payload 型別。有關如何使用此介面的更多資訊,請參閱 HMR API 文件

ts
type 
CustomFooPayload
=
InferCustomEventPayload
<'custom:foo'>
import.meta.
hot
?.
on
('custom:foo', (
payload
) => {
// The type of payload will be { msg: string } }) import.meta.
hot
?.
on
('unknown:event', (
payload
) => {
// The type of payload will be any })