外掛 API
Vite 外掛延伸了 Rollup 設計良好的外掛介面,並加入了一些 Vite 特有的選項。因此,您可以編寫一次 Vite 外掛,使其在開發和建構時都能運作。
建議先瀏覽 Rollup 的外掛文件,再閱讀以下章節。
撰寫外掛
Vite 致力於提供開箱即用的既有模式,因此在建立新的外掛之前,請務必查看功能指南,確認您的需求是否已涵蓋。同時也請檢閱可用的社群外掛,包括 相容的 Rollup 外掛 和 Vite 特定的外掛
在建立外掛時,您可以將其內嵌在您的 vite.config.js
中。不需要為其建立新的套件。當您發現某個外掛在您的專案中很有用時,請考慮分享它以幫助 生態系中的其他人。
提示
在學習、偵錯或撰寫外掛時,我們建議在您的專案中加入 vite-plugin-inspect。它可讓您檢查 Vite 外掛的中間狀態。安裝後,您可以訪問 localhost:5173/__inspect/
來檢查您專案的模組和轉換堆疊。請查看 vite-plugin-inspect 文件中的安裝說明。
慣例
如果外掛未使用 Vite 特定的 Hook,並且可以實作為相容的 Rollup 外掛,那麼建議使用 Rollup 外掛命名慣例。
- Rollup 外掛應具有明確的名稱,並以
rollup-plugin-
作為前綴。 - 在 package.json 中包含
rollup-plugin
和vite-plugin
關鍵字。
這會讓外掛也可以用於純 Rollup 或 WMR 為基礎的專案中
對於僅限 Vite 的外掛
- Vite 外掛應具有明確的名稱,並以
vite-plugin-
作為前綴。 - 在 package.json 中包含
vite-plugin
關鍵字。 - 在外掛文件中加入一個章節,詳細說明為何它是僅限 Vite 的外掛(例如,它使用了 Vite 特定的外掛 Hook)。
如果您的外掛僅適用於特定的框架,則其名稱應包含在字首中
- Vue 外掛的
vite-plugin-vue-
前綴 - React 外掛的
vite-plugin-react-
前綴 - Svelte 外掛的
vite-plugin-svelte-
前綴
另請參閱 虛擬模組慣例。
外掛配置
使用者會將外掛新增至專案的 devDependencies
,並使用 plugins
陣列選項進行配置。
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
export default defineConfig({
plugins: [vitePlugin(), rollupPlugin()],
})
Falsy 的外掛將被忽略,可用於輕鬆啟用或停用外掛。
plugins
也接受包含多個外掛的預設設定,作為單一元素。這對於使用多個外掛實作的複雜功能(如框架整合)很有用。陣列將在內部扁平化。
// 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)]
}
import { defineConfig } from 'vite'
import framework from 'vite-plugin-framework'
export default defineConfig({
plugins: [framework()],
})
簡單範例
提示
通常的慣例是將 Vite/Rollup 外掛撰寫為會傳回實際外掛物件的工廠函式。函式可以接受選項,讓使用者自訂外掛的行為。
轉換自訂檔案類型
const fileRegex = /\.(my-file-ext)$/
export default function myPlugin() {
return {
name: 'transform-file',
transform(src, id) {
if (fileRegex.test(id)) {
return {
code: compileFileToJS(src),
map: null, // provide source map if available
}
}
},
}
}
匯入虛擬檔案
請參閱下一節中的範例。
虛擬模組慣例
虛擬模組是一種有用的方案,可讓您使用一般的 ESM 匯入語法將建構時間資訊傳遞至原始檔。
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(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export const msg = "from virtual module"`
}
},
}
}
允許在 JavaScript 中匯入模組
import { msg } from 'virtual:my-module'
console.log(msg)
Vite(和 Rollup)中的虛擬模組,按照慣例,使用者可見的路徑會加上 virtual:
作為字首。如果可能,外掛名稱應作為命名空間,以避免與生態系中的其他外掛發生衝突。例如,vite-plugin-posts
可以要求使用者匯入 virtual:posts
或 virtual:posts/helpers
虛擬模組,以取得建構時間資訊。在內部,使用虛擬模組的外掛在解析 ID 時,應以 \0
作為模組 ID 的字首,這是來自 rollup 生態系的慣例。這可防止其他外掛嘗試處理 ID(如節點解析),而像 sourcemap 之類的核心功能可以使用此資訊來區分虛擬模組和常規檔案。\0
在匯入 URL 中不允許使用,因此我們必須在匯入分析期間取代它們。在瀏覽器中開發時,\0{id}
虛擬 ID 會被編碼為 /@id/__x00__{id}
。ID 會在進入外掛管線之前被解碼回來,因此外掛 Hook 程式碼看不到它。
請注意,直接衍生自實際檔案的模組,如單一檔案元件(如 .vue 或 .svelte SFC)中的腳本模組,不需要遵循此慣例。SFC 通常在處理時會產生一組子模組,但這些模組中的程式碼可以對應回檔案系統。針對這些子模組使用 \0
會阻止 sourcemap 正常運作。
通用 Hook
在開發期間,Vite 開發伺服器會建立一個外掛容器,以與 Rollup 相同的方式調用 Rollup 建構 Hook。
以下 Hook 在伺服器啟動時呼叫一次
以下 Hook 會在每次傳入模組請求時呼叫
這些 Hook 還有一個擴充的 options
參數,其中包含其他 Vite 特定的屬性。您可以在SSR 文件中閱讀更多內容。
由於 Vite 的非捆綁開發伺服器模式,對於根目錄中一般 index.html
的某些 resolveId
呼叫,其 importer
值可能會是絕對路徑,因為並非總是能夠衍生出實際的匯入器。對於 Vite 解析管線中處理的匯入,匯入器可以在匯入分析階段追蹤,以提供正確的 importer
值。
以下 Hook 會在伺服器關閉時呼叫
請注意,由於 Vite 避免完整 AST 解析以獲得更好的效能,因此在開發期間不會呼叫 moduleParsed
Hook。
在開發期間不會呼叫輸出生成 Hook(除了 closeBundle
之外)。您可以將 Vite 的開發伺服器視為僅呼叫 rollup.rollup()
而不呼叫 bundle.generate()
。
Vite 特定的 Hook
Vite 外掛也可以提供用於 Vite 特定用途的 Hook。這些 Hook 會被 Rollup 忽略。
config
類型:
(config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void
類型:
async
、sequential
在解析 Vite 配置之前修改它。Hook 接收原始使用者配置(與配置檔案合併的 CLI 選項)和目前配置環境,該環境公開正在使用的
mode
和command
。它可以傳回將會深層合併到現有配置中的部分配置物件,或直接變更配置(如果預設合併無法達成所需的結果)。範例
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' } }, })
注意
使用者外掛會在執行此 Hook 之前解析,因此在
config
Hook 中注入其他外掛將沒有效果。
configResolved
類型:
(config: ResolvedConfig) => void | Promise<void>
類型:
async
、parallel
在解析 Vite 配置後呼叫。使用此 Hook 來讀取和儲存最終解析的配置。當外掛需要根據正在執行的命令執行不同的操作時,它也很有用。
範例
jsconst 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 中,vite
、vite dev
和vite serve
是別名)。
configureServer
類型:
(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>
類型:
async
、sequential
另請參閱: ViteDevServer
用於設定開發伺服器的 Hook。最常見的用途是將自訂中介軟體添加到內部 connect 應用程式中
jsconst myPlugin = () => ({ name: 'configure-server', configureServer(server) { server.middlewares.use((req, res, next) => { // custom handle request... }) }, })
注入後置中介軟體
configureServer
Hook 會在安裝內部中介軟體之前呼叫,因此預設情況下,自訂中介軟體會在內部中介軟體之前執行。如果想要在內部中介軟體之後注入中介軟體,可以從configureServer
返回一個函式,該函式將在安裝內部中介軟體之後呼叫jsconst 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... }) } }, })
儲存伺服器存取權限
在某些情況下,其他外掛程式 Hook 可能需要存取開發伺服器實例 (例如,存取 WebSocket 伺服器、檔案系統監看程式或模組圖)。此 Hook 也可用於儲存伺服器實例,以便在其他 Hook 中存取
jsconst myPlugin = () => { let server return { name: 'configure-server', configureServer(_server) { server = _server }, transform(code, id) { if (server) { // use server... } }, } }
請注意,當執行生產版本時,不會呼叫
configureServer
,因此您的其他 Hook 需要防範其不存在的情況。
configurePreviewServer
類型:
(server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>
類型:
async
、sequential
另請參閱: PreviewServer
與
configureServer
相同,但用於預覽伺服器。與configureServer
類似,configurePreviewServer
Hook 會在安裝其他中介軟體之前呼叫。如果想要在其他中介軟體之後注入中介軟體,可以從configurePreviewServer
返回一個函式,該函式將在安裝內部中介軟體之後呼叫jsconst 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
)的專用 Hook。此 Hook 接收目前的 HTML 字串和轉換內容。此內容在開發期間會公開ViteDevServer
實例,並在建置期間公開 Rollup 輸出套件。此 Hook 可以是非同步的,並可以返回下列其中之一
- 已轉換的 HTML 字串
- 要注入到現有 HTML 中的標籤描述符物件陣列 (
{ tag, attrs, children }
)。每個標籤也可以指定其應注入的位置 (預設為預先添加到<head>
) - 包含兩者的物件,如
{ html, tags }
預設情況下,
order
為undefined
,此 Hook 會在 HTML 轉換完成後套用。為了注入應通過 Vite 外掛程式管道的腳本,order: 'pre'
將在處理 HTML 之前套用此 Hook。order: 'post'
將在套用所有order
未定義的 Hook 後套用此 Hook。基本範例
jsconst htmlPlugin = () => { return { name: 'html-transform', transformIndexHtml(html) { return html.replace( /<title>(.*?)<\/title>/, `<title>Title replaced!</title>`, ) }, } }
完整的 Hook 簽章
tstype 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 attrs?: Record<string, string | boolean> children?: string | HtmlTagDescriptor[] /** * default: 'head-prepend' */ injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend' }
注意
如果使用自訂處理進入檔案的框架(例如 SvelteKit),則不會呼叫此 Hook。
handleHotUpdate
類型:
(ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
另請參閱: HMR API
執行自訂 HMR 更新處理。此 Hook 接收具有下列簽章的內容物件
tsinterface HmrContext { file: string timestamp: number modules: Array<ModuleNode> read: () => string | Promise<string> server: ViteDevServer }
modules
是受變更檔案影響的模組陣列。這是一個陣列,因為單個檔案可能會對應到多個服務的模組 (例如,Vue SFC)。read
是返回檔案內容的非同步讀取函式。提供此函式是因為在某些系統上,檔案變更回呼可能會在編輯器完成更新檔案之前太快觸發,並且直接fs.readFile
將返回空內容。傳遞的讀取函式會正規化此行為。
此 Hook 可以選擇
篩選並縮小受影響的模組清單,以使 HMR 更準確。
返回空陣列並執行完整重新載入
jshandleHotUpdate({ 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 處理
jshandleHotUpdate({ server }) { server.ws.send({ type: 'custom', event: 'special-update', data: {} }) return [] }
用戶端程式碼應使用 HMR API 註冊對應的處理常式(這可以由同一外掛程式的
transform
Hook 注入)jsif (import.meta.hot) { import.meta.hot.on('special-update', (data) => { // perform custom update }) }
外掛程式排序
Vite 外掛程式還可以指定 enforce
屬性(類似於 webpack 加載器)來調整其應用程式順序。enforce
的值可以是 "pre"
或 "post"
。已解析的外掛程式將按以下順序排列
- 別名
- 具有
enforce: 'pre'
的使用者外掛程式 - Vite 核心外掛程式
- 沒有 enforce 值的使用者外掛程式
- Vite 建置外掛程式
- 具有
enforce: 'post'
的使用者外掛程式 - Vite 後建置外掛程式(縮小、資訊清單、報告)
請注意,這與 Hook 排序是分開的,這些 Hook 仍然單獨受其 order
屬性影響 與 Rollup Hook 的慣例相同。
條件式應用
預設情況下,外掛程式會針對伺服和建置呼叫。如果外掛程式需要在伺服或建置期間有條件地套用,請使用 apply
屬性以僅在 'build'
或 'serve'
期間呼叫它們
function myPlugin() {
return {
name: 'build-only',
apply: 'build', // or 'serve'
}
}
函式也可以用於更精確的控制
apply(config, { command }) {
// apply only on build but not for SSR
return command === 'build' && !config.build.ssr
}
Rollup 外掛程式相容性
許多 Rollup 外掛程式可以直接作為 Vite 外掛程式使用 (例如,@rollup/plugin-alias
或 @rollup/plugin-json
),但並非所有外掛程式都可以,因為某些外掛程式 Hook 在未捆綁的開發伺服器環境中沒有意義。
一般而言,只要 Rollup 外掛程式符合下列條件,它就應該可以作為 Vite 外掛程式使用
- 它不使用
moduleParsed
Hook。 - 它在捆綁階段 Hook 和輸出階段 Hook 之間沒有強烈的耦合。
如果 Rollup 外掛程式僅對建置階段有意義,則可以在 build.rollupOptions.plugins
下指定它。它的運作方式與具有 enforce: 'post'
和 apply: 'build'
的 Vite 外掛程式相同。
您還可以通過 Vite 特有的屬性來擴充現有的 Rollup 外掛程式
import example from 'rollup-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/pluginutils
的 normalizePath
實用程式函式,該函式會在執行比較之前將分隔符號轉換為 POSIX。這表示當這些外掛程式在 Vite 中使用時,include
和 exclude
設定模式以及其他針對解析的 ID 比較的類似路徑可以正確運作。
因此,對於 Vite 外掛程式,在比較解析的 ID 路徑時,務必先將路徑正規化為使用 POSIX 分隔符號。等效的 normalizePath
實用程式函式會從 vite
模組匯出。
import { normalizePath } from 'vite'
normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'
篩選,包含/排除模式
Vite 公開 @rollup/pluginutils
的 createFilter
函式,以鼓勵 Vite 特有的外掛程式和整合使用標準包含/排除篩選模式,該模式也在 Vite 核心本身中使用。
用戶端-伺服器通訊
自 Vite 2.9 起,我們提供了一些實用程式,以協助外掛程式處理與用戶端的通訊。
伺服器到用戶端
在外掛程式方面,我們可以使用 server.ws.send
向用戶端廣播事件
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
server.ws.on('connection', () => {
server.ws.send('my:greetings', { msg: 'hello' })
})
},
},
],
})
注意
我們建議始終為事件名稱加上前置詞,以避免與其他外掛程式發生衝突。
在用戶端方面,請使用 hot.on
來監聽事件
// client side
if (import.meta.hot) {
import.meta.hot.on('my:greetings', (data) => {
console.log(data.msg) // hello
})
}
用戶端到伺服器
若要從用戶端向伺服器發送事件,我們可以使用 hot.send
// client side
if (import.meta.hot) {
import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}
然後使用 server.ws.on
並在伺服器端監聽事件
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
介面推斷有效負載的類型,可以通過擴充介面來為自訂事件鍵入
注意
指定 TypeScript 宣告檔案時,請務必包含 .d.ts
延伸。否則,TypeScript 可能不知道模組要擴充哪個檔案。
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
的有效負載類型。有關如何利用此介面的更多資訊,請參閱 HMR API 文件。
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
})