插件 API
插件 API
Vite 插件在 Rolldown 插件接口的基础上扩展了一些 Vite 特有的选项。因此,您可以编写一次 Vite 插件,并使其同时适用于开发和生产构建。
在阅读以下章节之前,建议先阅读 Rolldown 的插件文档。
编写插件
Vite 致力于提供开箱即用的成熟模式,因此在创建新插件之前,请先检查 功能指南,确认您的需求是否已经得到满足。同时,也请查看现有的社区插件,包括兼容的 Rollup 插件和 Vite 特有插件。
创建插件时,您可以将其内联在 vite.config.js 中。无需为此创建新包。当您发现某个插件在项目中非常有用时,可以考虑将其分享出来,帮助生态社区中的其他人。
::: tip 提示
在学习、调试或编写插件时,我们建议在项目中引入 vite-plugin-inspect。它允许您检查 Vite 插件的中间状态。安装后,您可以访问 localhost:5173/__inspect/ 来检查项目的模块和转换堆栈。请参阅 vite-plugin-inspect 文档中的安装说明。

约定
如果插件未使用 Vite 特有的钩子,并且可以作为一个兼容的 Rolldown 插件来实现,那么建议遵循 Rolldown 插件命名约定。
- Rolldown 插件应具有以
rolldown-plugin-前缀开头的明确名称。- 在 package.json 的
keywords字段中包含rolldown-plugin和vite-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数组选项进行配置。
js [vite.config.js]import vitePlugin from 'vite-plugin-feature' import rollupPlugin from 'rollup-plugin-feature' export default defineConfig({ plugins: [vitePlugin(), rollupPlugin()], })值为假(falsy)的插件将被忽略,这可用于轻松地启用或停用插件。
plugins也接受预设(presets),这些预设将多个插件作为单个元素。这对于通过多个插件实现的复杂功能(如框架集成)非常有用。该数组将在内部被扁平化。
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)] }
js [vite.config.js]import { defineConfig } from 'vite' import framework from 'vite-plugin-framework' export default defineConfig({ plugins: [framework()], })简单示例tip 提示
编写 Vite/Rolldown/Rollup 插件时,通常约定俗成地将其编写为一个返回实际插件对象的工厂函数。该函数可以接受选项,允许用户自定义插件的行为。
转换自定义文件类型
jsconst 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, // 如果可用,请提供 source map } }, }, } }导入虚拟文件
虚拟模块允许您使用常规的 ESM 导入语法将构建时信息传递给源文件。完整约定请参阅 虚拟模块约定。
jsimport { exactRegex } from '@rolldown/pluginutils' export default function myPlugin() { const virtualModuleId = 'virtual:my-module' const resolvedVirtualModuleId = '\0' + virtualModuleId return { name: 'my-plugin', // 必填,将显示在警告和错误中 resolveId: { filter: { id: exactRegex(virtualModuleId) }, handler() { return resolvedVirtualModuleId }, }, load: { filter: { id: exactRegex(resolvedVirtualModuleId) }, handler() { return `export const msg = "from virtual module"` }, }, } }这样即可在 JavaScript 中导入该模块:
jsimport { msg } from 'virtual:my-module' console.log(msg)在 Vite 中,由于
\0不是导入 URL 中允许的字符,因此在开发环境中,\0{id}虚拟 ID 最终会在浏览器中被编码为/@id/__x00__{id}。在进入插件流水线之前,该 ID 会被解码回来,因此插件钩子代码不会看到这一点。Rolldown 钩子
在开发模式下,Vite 开发服务器会创建一个插件容器,以与 Rolldown 相同的方式调用 Rolldown 构建钩子。
所有 Rolldown 钩子都是按环境(per-environment)的钩子。
以下钩子在服务器启动时调用一次:
以下钩子在每次传入模块请求时调用:
这些钩子还有一个扩展的
options参数,其中包含额外的 Vite 特有属性。您可以在 SSR 文档中了解更多信息。某些
resolveId调用的importer值可能是根目录下通用index.html的绝对路径,因为 Vite 的免打包开发服务器模式并不总能推导出实际的导入者。对于在 Vite 的解析流水线中处理的导入,可以在导入分析阶段跟踪导入者,从而提供正确的importer值。以下钩子在服务器关闭时调用:
请注意,
moduleParsed钩子在开发模式下不会被调用,因为 Vite 为了获得更好的性能会避免完整的 AST 解析。输出生成钩子(
closeBundle除外)在开发模式下不会被调用。Vite 特有钩子
Vite 插件还可以提供服务于 Vite 特定目的的钩子。这些钩子会被 Rollup 忽略。
config
类型:
(config: UserConfig, env: { mode: 'build' | 'serve', command: string, isSsrBuild?: boolean, isPreview?: boolean }) => UserConfig | null | void类型:
async,sequential作用域: 全局(Global)
在 Vite 配置被解析之前修改它。该钩子接收原始用户配置(命令行选项与配置文件合并后)以及当前的配置环境,后者会暴露正在使用的
mode和command。它可以返回一个部分配置对象,该对象将被深度合并到现有配置中;也可以直接修改配置(如果默认的合并无法达到预期效果)。示例:
js// 返回部分配置(推荐) const partialConfigPlugin = () => ({ name: 'return-partial', config: () => ({ resolve: { alias: { foo: 'bar', }, }, }), }) // 直接修改配置(仅在合并不起作用时使用) const mutateConfigPlugin = () => ({ name: 'mutate-config', config(config, { command }) { if (command === 'build') { config.root = 'foo' } }, })::: warning 注意
用户插件在此钩子运行之前已被解析,因此在config钩子中注入其他插件将不会生效。
:::
configResolved
类型:
(config: ResolvedConfig) => void | Promise<void>类型:
async,parallel作用域: 全局(Global)
在 Vite 配置解析完成后调用。使用此钩子来读取和存储最终解析的配置。当插件需要根据正在运行的命令执行不同操作时,此钩子也非常有用。
示例:
jsconst examplePlugin = () => { let config return { name: 'read-config', configResolved(resolvedConfig) { // 存储解析后的配置 config = resolvedConfig }, // 在其他钩子中使用存储的配置 transform(code, id) { if (config.command === 'serve') { // dev:插件由开发服务器调用 } else { // build:插件由 Rollup 调用 } }, } }请注意,在开发模式下(命令行中
vite、vite dev和vite serve是别名),command值为serve。
configureServer
类型:
(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>类型:
async,sequential另请参阅: ViteDevServer
作用域: 全局(Global)
用于配置开发服务器的钩子。最常见的用例是向内部的 connect 应用添加自定义中间件:
jsconst myPlugin = () => ({ name: 'configure-server', configureServer(server) { server.middlewares.use((req, res, next) => { // 自定义请求处理... }) }, })注入后置中间件
configureServer钩子在内部中间件安装之前被调用,因此默认情况下自定义中间件会在内部中间件之前运行。如果您想在内部中间件之后注入中间件,可以从configureServer返回一个函数,该函数将在内部中间件安装完成后被调用:
jsconst myPlugin = () => ({ name: 'configure-server', configureServer(server) { // 返回一个后置钩子,在内部中间件安装完成后被调用 return () => { server.middlewares.use((req, res, next) => { // 自定义请求处理... }) } }, })存储服务器访问
在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 WebSocket 服务器、文件系统监视器或模块图)。此钩子也可以用于存储服务器实例,以便在其他钩子中访问:
jsconst myPlugin = () => { let server return { name: 'configure-server', configureServer(_server) { server = _server }, transform(code, id) { if (server) { // 使用服务器... } }, } }请注意,
configureServer在生产构建时不会调用,因此您的其他钩子需要防护其不存在的情况。
configurePreviewServer
类型:
(server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>类型:
async,sequential另请参阅: PreviewServer
作用域: 全局(Global)
与
configureServer相同,但用于预览服务器。与configureServer类似,configurePreviewServer钩子在其他中间件安装之前被调用。如果您想在其他中间件之后注入中间件,可以从configurePreviewServer返回一个函数,该函数将在内部中间件安装完成后被调用:
jsconst myPlugin = () => ({ name: 'configure-preview-server', configurePreviewServer(server) { // 返回一个后置钩子,在其他中间件安装完成后被调用 return () => { server.middlewares.use((req, res, next) => { // 自定义请求处理... }) } }, })
transformIndexHtml
类型:
IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }类型:
async,sequential作用域: 按环境(Per-environment)
用于转换 HTML 入口文件(如
index.html)的专用钩子。该钩子接收当前的 HTML 字符串和一个转换上下文。该上下文在开发时暴露ViteDevServer实例,在构建时暴露 Rollup 的输出 bundle。该钩子可以是异步的,并且可以返回以下之一:
- 转换后的 HTML 字符串
- 一组标签描述对象(
{ tag, attrs, children }),用于注入到现有 HTML 中。每个标签还可以指定注入位置(默认是前置到<head>)- 一个同时包含两者的对象
{ html, tags }默认情况下
order为undefined,此时该钩子在 HTML 被转换后应用。为了注入一个应该经过 Vite 插件流水线的脚本,order: 'pre'会在处理 HTML 之前应用该钩子。order: 'post'会在所有order为undefined的钩子应用之后应用该钩子。基本示例:
jsconst htmlPlugin = () => { return { name: 'html-transform', transformIndexHtml(html) { return html.replace( /<title>(.*?)<\/title>/, `<title>Title replaced!</title>`, ) }, } }完整钩子签名:
tstype IndexHtmlTransformHook = ( html: string, ctx: { path: string filename: string server?: ViteDevServer bundle?: import('rolldown').OutputBundle chunk?: import('rolldown').OutputChunk originalUrl?: string }, ) => 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[] /** * 默认值:'head-prepend' */ injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend' }::: warning 注意
如果您使用的框架对入口文件有自定义处理(例如 SvelteKit),此钩子将不会被调用。
:::
handleHotUpdate
类型:
(ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>类型:
async,sequential另请参阅: HMR API
作用域: 按环境(Per-environment)
执行自定义 HMR 更新处理。该钩子接收一个上下文对象,其签名如下:
tsinterface HmrContext { file: string timestamp: number modules: Array<ModuleNode> read: () => string | Promise<string> server: ViteDevServer }
modules是受更改文件影响的模块数组。它是一个数组,因为单个文件可能映射到多个服务的模块(例如 Vue SFC)。read是一个异步读取函数,返回文件的内容。提供此函数是因为在某些系统上,文件更改回调可能在编辑器完成更新文件之前就触发了,直接使用fs.readFile将返回空内容。传入的 read 函数会规范化此行为。该钩子可以选择:
过滤并缩小受影响的模块列表,使 HMR 更精确。
返回一个空数组并执行完全重新加载:
jshandleHotUpdate({ server, modules, timestamp }) { // 手动使模块失效 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钩子注入):
jsif (import.meta.hot) { import.meta.hot.on('special-update', (data) => { // 执行自定义更新 }) }插件上下文元数据
对于可以访问插件上下文的插件钩子,Vite 在
this.meta上暴露了额外的属性:
this.meta.viteVersion:当前 Vite 版本字符串(例如"8.0.0")。 tip 检测 Rolldown 驱动的 Vite
this.meta.rolldownVersion 仅适用于由 Rolldown 驱动的 Vite(即 Vite 8+)。您可以使用它来检测当前 Vite 实例是否由 Rolldown 驱动:
ts
function versionCheckPlugin(): Plugin {
return {
name: 'version-check',
buildStart() {
if (this.meta.rolldownVersion) {
// 仅在由 Rolldown 驱动的 Vite 上执行某些操作
} else {
// 在由 Rollup 驱动的 Vite 上执行其他操作
}
},
}
}
输出包元数据
在构建期间,Vite 会使用 Vite 特有的
viteMetadata字段增强 Rolldown 的构建输出对象。这可以通过以下方式获得:
RenderedChunk(例如在renderChunk和augmentChunkHash中)OutputChunk和OutputAsset(例如在generateBundle和writeBundle中)
viteMetadata提供:
viteMetadata.importedCss: Set<string>viteMetadata.importedAssets: Set<string>这在编写需要检查发出的 CSS 和静态资源而无需依赖
build.manifest的插件时非常有用。示例:
ts [vite.config.ts]function outputMetadataPlugin(): Plugin { return { name: 'output-metadata-plugin', enforce: 'post', 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 的 loader)来调整其应用顺序。enforce的值可以是"pre"或"post"。解析后的插件将按以下顺序排列:
- 别名(Alias)
enforce: 'pre'的用户插件- Vite 核心插件
- 未设置 enforce 值的用户插件
- Vite 构建插件
enforce: 'post'的用户插件- Vite 后置构建插件(压缩(minify)、manifest、报告)
请注意,这与钩子排序是分开的,钩子排序仍然照常遵循 Rolldown 钩子的
order属性。条件应用
默认情况下,插件在 serve 和 build 时都会被调用。在插件需要仅在 serve 或 build 期间有条件地应用的情况下,可以使用
apply属性使其仅在'build'或'serve'期间被调用:
jsfunction myPlugin() { return { name: 'build-only', apply: 'build', // 或 'serve' } }也可以使用函数进行更精确的控制:
jsapply(config, { command }) { // 仅在构建时应用,但不适用于 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 阶段钩子和输出阶段钩子之间没有强耦合。
如果一个 Rolldown / Rollup 插件只在构建阶段有意义,则可以将其放在
build.rolldownOptions.plugins下。它的工作方式与带有enforce: 'post'和apply: 'build'的 Vite 插件相同。您还可以为现有的 Rolldown / Rollup 插件添加 Vite 特有的属性:
js [vite.config.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 默认保持已解析路径不变,因此在 Windows 上,已解析的 ID 会带有 win32 分隔符( \ )。然而,Rollup 插件在内部使用
@rollup/pluginutils的normalizePath工具函数,该函数在执行比较之前会将分隔符转换为 POSIX。这意味着当这些插件在 Vite 中使用时,include和exclude配置模式以及针对已解析 ID 的类似路径比较可以正常工作。因此,对于 Vite 插件,在将路径与已解析的 ID 进行比较时,务必先将路径规范化为使用 POSIX 分隔符。
vite模块导出了一个等效的normalizePath工具函数。
jsimport { normalizePath } from 'vite' normalizePath('foo\\bar') // 'foo/bar' normalizePath('foo/bar') // 'foo/bar'过滤,include/exclude 模式
Vite 暴露了
@rollup/pluginutils的createFilter函数,以鼓励 Vite 特有插件和集成使用标准的 include/exclude 过滤模式,Vite 核心本身也使用这种模式。钩子过滤器
Rolldown 引入了钩子过滤器特性,以减少 Rust 和 JavaScript 运行时之间的通信开销。此功能允许插件指定决定何时应调用钩子的模式,通过避免不必要的钩子调用来提高性能。
Rollup 4.38.0+ 和 Vite 6.3.0+ 也支持此功能。为了使您的插件向后兼容旧版本,请确保在钩子处理程序内也运行过滤器。
jsexport default function myPlugin() { const jsFileRegex = /\.js$/ return { name: 'my-plugin', // 示例:仅对 .js 文件调用 transform transform: { filter: { id: jsFileRegex, }, handler(code, id) { // 向后兼容的附加检查 if (!jsFileRegex.test(id)) return null return { code: transformCode(code), map: null, } }, }, } } ``` tip 提示
@rolldown/pluginutils 为钩子过滤器导出了一些工具函数,例如 exactRegex 和 prefixRegex。为了使用方便,这些函数也从 rolldown/filter 中重新导出。
分块导入映射信息info 实验性功能
此功能为实验性,未来可能会发生变化。
当启用
build.chunkImportMap选项时,生成的分块中的导入语句将使用每个分块的唯一 ID,而不是文件路径。要获取从分块 ID 到文件路径的映射,您可以在
generateBundle钩子或writeBundle钩子中访问已发出的包中的导入映射。导入映射的名称由build.rolldownOptions.experimental.chunkImportMap.fileName指定(默认为importmap.json)。
tsfunction accessImportMap() { let config: ResolvedConfig return { name: 'access-import-map', configResolved(resolvedConfig) { config = resolvedConfig }, generateBundle(options, bundle) { const chunkImportMap = config.build.rolldownOptions.experimental?.chunkImportMap if (chunkImportMap) { const importMapFilename = typeof chunkImportMap === 'object' && chunkImportMap.fileName ? chunkImportMap.fileName : 'importmap.json' const importMap = bundle[importMapFilename]! as OutputAsset const mapping = JSON.parse(importMap.source).imports console.log(mapping) // { "./entry.hash1.js": "./entry.hash2.js" } } }, } }客户端-服务器通信
从 Vite 2.9 开始,我们提供了一些工具函数,帮助插件处理与客户端的通信。
服务器到客户端
在插件端,我们可以使用
server.ws.send向客户端广播事件:
js [vite.config.js]export default defineConfig({ plugins: [ { // ... configureServer(server) { server.ws.on('connection', () => { server.ws.send('my:greetings', { msg: 'hello' }) }) }, }, ], }) ``` tip 注意
我们建议始终为事件名称添加前缀,以避免与其他插件发生冲突。
在客户端,使用
hot.on监听事件:
ts twoslashimport 'vite/client' // ---cut--- // 客户端 if (import.meta.hot) { import.meta.hot.on('my:greetings', (data) => { console.log(data.msg) // hello }) }客户端到服务器
要将事件从客户端发送到服务器,我们可以使用
hot.send:
ts// 客户端 if (import.meta.hot) { import.meta.hot.send('my:from-client', { msg: 'Hey!' }) }然后使用
server.ws.on在服务器端监听事件:
js [vite.config.js]export default defineConfig({ plugins: [ { // ... configureServer(server) { server.ws.on('my:from-client', (data, client) => { console.log('Message from client:', data.msg) // Hey! // 仅回复该客户端(如果需要) client.send('my:ack', { msg: 'Hi! I got your message!' }) }) }, }, ], })自定义事件的 TypeScript
在内部,Vite 从
CustomEventMap接口推断负载的类型,您可以通过扩展该接口来为自定义事件添加类型:tip 注意
在指定 TypeScript 声明文件时,请确保包含.d.ts扩展名。否则,TypeScript 可能不知道模块试图扩展哪个文件。
:::
ts [events.d.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 的负载类型。有关此接口如何被使用的更多信息,请参阅 HMR API 文档。
ts twoslash
import 'vite/client'
import type { InferCustomEventPayload } from 'vite/types/customEvent.d.ts'
declare module 'vite/types/customEvent.d.ts' {
interface CustomEventMap {
'custom:foo': { msg: string }
}
}
// ---cut---
type CustomFooPayload = InferCustomEventPayload<'custom:foo'>
import.meta.hot?.on('custom:foo', (payload) => {
// payload 的类型将是 { msg: string }
})
import.meta.hot?.on('unknown:event', (payload) => {
// payload 的类型将是 any
})
帮助我们改进文档
发现翻译问题或内容错误?请告诉我们。
