插件 API
Vite 插件扩展了 Rollup 的设计良好的插件接口,并添加了一些额外的 Vite 特定选项。因此,您可以编写一次 Vite 插件,使其在开发和构建中都能正常工作。
建议您在阅读以下部分之前先阅读 Rollup 的插件文档。
编写插件
Vite 努力在开箱即用时提供既定的模式,因此在创建新的插件之前,请确保您检查 功能指南,看看您的需求是否已涵盖。此外,请查看可用的社区插件,包括 兼容的 Rollup 插件 和 Vite 特定插件
创建插件时,您可以在 vite.config.js
中内联它。无需为它创建新的包。一旦您发现插件在您的项目中很有用,请考虑分享它以帮助其他人 在生态系统中。
提示
在学习、调试或编写插件时,我们建议您在项目中包含 vite-plugin-inspect。它允许您检查 Vite 插件的中间状态。安装后,您可以访问 localhost:5173/__inspect/
以检查项目的模块和转换堆栈。查看 vite-plugin-inspect 文档 中的安装说明。
约定
如果插件不使用 Vite 特定的钩子,并且可以作为 兼容的 Rollup 插件 实现,那么建议使用 Rollup 插件命名约定。
- Rollup 插件应该有一个清晰的名称,带有
rollup-plugin-
前缀。 - 在 package.json 中包含
rollup-plugin
和vite-plugin
关键字。
这将使插件在纯 Rollup 或 WMR 驱动的项目中也能使用。
对于仅限 Vite 的插件
- Vite 插件应该有一个清晰的名称,带有
vite-plugin-
前缀。 - 在 package.json 中包含
vite-plugin
关键字。 - 在插件文档中包含一个部分,详细说明为什么它是仅限 Vite 的插件(例如,它使用 Vite 特定的插件钩子)。
如果您的插件只适用于特定框架,则其名称应包含在该前缀中。
vite-plugin-vue-
前缀用于 Vue 插件vite-plugin-react-
前缀用于 React 插件vite-plugin-svelte-
前缀用于 Svelte 插件
另请参阅 虚拟模块约定。
插件配置
用户将插件添加到项目 devDependencies
中,并使用 plugins
数组选项配置它们。
// vite.config.js
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'
export default defineConfig({
plugins: [vitePlugin(), rollupPlugin()],
})
假值插件将被忽略,这可以用来轻松地激活或停用插件。
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)]
}
// vite.config.js
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 时应在模块 ID 前添加 \0
,这是 Rollup 生态系统中的一个约定。这可以防止其他插件尝试处理 ID(如节点解析),并且核心功能(如源映射)可以使用此信息来区分虚拟模块和普通文件。\0
是导入 URL 中不允许的字符,因此我们在导入分析期间必须替换它们。\0{id}
虚拟 ID 在开发过程中最终在浏览器中被编码为 /@id/__x00__{id}
。在进入插件管道之前,ID 将被解码回,因此插件钩子代码看不到它。
请注意,直接从真实文件派生的模块(如单文件组件中的脚本模块(如 .vue 或 .svelte SFC))不需要遵循此约定。SFC 通常在处理时生成一组子模块,但这些子模块中的代码可以映射回文件系统。对这些子模块使用 \0
会阻止源映射正常工作。
通用钩子
在开发过程中,Vite 开发服务器创建一个插件容器,该容器以与 Rollup 相同的方式调用 Rollup 构建钩子。
以下钩子在服务器启动时调用一次
以下钩子在每个传入的模块请求时调用
这些钩子还具有一个扩展的 options
参数,其中包含额外的 Vite 特定属性。您可以在 SSR 文档 中了解更多信息。
某些 resolveId
调用的 importer
值可能是根目录下通用 index.html
的绝对路径,因为由于 Vite 的非捆绑式开发服务器模式,无法始终推断出实际的导入程序。对于在 Vite 的解析管道中处理的导入,导入程序可以在导入分析阶段进行跟踪,从而提供正确的 importer
值。
以下钩子在服务器关闭时调用
请注意,moduleParsed
钩子在开发过程中不会被调用,因为 Vite 避免了完整的 AST 解析以提高性能。
输出生成钩子(除了 closeBundle
)在开发过程中不会被调用。您可以将 Vite 的开发服务器视为只调用 rollup.rollup()
而没有调用 bundle.generate()
。
Vite 特定钩子
Vite 插件还可以提供用于特定于 Vite 的目的的钩子。这些钩子会被 Rollup 忽略。
config
类型:
(config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void
类型:
async
,sequential
在 Vite 配置解析之前修改它。该钩子接收原始用户配置(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' } }, })
注意
用户插件在运行此钩子之前解析,因此在
config
钩子内注入其他插件将无效。
configResolved
类型:
(config: ResolvedConfig) => void | Promise<void>
类型:
async
,parallel
在 Vite 配置解析后调用。使用此钩子读取和存储最终解析的配置。当插件需要根据正在运行的命令执行不同的操作时,它也很有用。
示例
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
用于配置开发服务器的钩子。最常见的用例是向内部 connect 应用程序添加自定义中间件
jsconst myPlugin = () => ({ name: 'configure-server', configureServer(server) { server.middlewares.use((req, res, next) => { // custom handle request... }) }, })
注入后中间件
configureServer
钩子在安装内部中间件之前调用,因此默认情况下自定义中间件将在内部中间件之前运行。如果要在内部中间件之后注入中间件,可以从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... }) } }, })
存储服务器访问
在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如,访问 WebSocket 服务器、文件系统监视器或模块图)。此钩子也可用于存储服务器实例,以便在其他钩子中访问
jsconst 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
返回一个函数,该函数将在安装内部中间件后调用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
)的钩子。该钩子接收当前 HTML 字符串和一个转换上下文。该上下文在开发期间公开ViteDevServer
实例,并在构建期间公开 Rollup 输出包。该钩子可以是异步的,并且可以返回以下内容之一
- 转换后的 HTML 字符串
- 要注入到现有 HTML 中的标签描述符对象数组(
{ tag, attrs, children }
)。每个标签还可以指定它应该注入的位置(默认情况下是追加到<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('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' }
handleHotUpdate
类型:
(ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
另请参阅: HMR API
执行自定义 HMR 更新处理。该钩子接收一个具有以下签名的上下文对象
tsinterface HmrContext { file: string timestamp: number modules: Array<ModuleNode> read: () => string | Promise<string> server: ViteDevServer }
modules
是受更改文件影响的模块数组。它是一个数组,因为单个文件可能映射到多个已服务模块(例如 Vue SFC)。read
是一个异步读取函数,它返回文件的內容。提供此函数是因为在某些系统上,文件更改回调可能会在编辑器完成更新文件之前过快地触发,而直接fs.readFile
将返回空内容。传入的读取函数规范化了这种行为。
该钩子可以选择
过滤并缩小受影响的模块列表,使 HMR 更准确。
返回一个空数组并执行完全重新加载
jshandleHotUpdate({ server, modules, timestamp }) { // Also use `server.ws.send` to support Vite <5.1 if needed server.hot.send({ type: 'full-reload' }) // Invalidate modules manually const invalidatedModules = new Set() for (const mod of modules) { server.moduleGraph.invalidateModule( mod, invalidatedModules, timestamp, true ) } return [] }
返回一个空数组并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理
jshandleHotUpdate({ server }) { // Also use `server.ws.send` to support Vite <5.1 if needed server.hot.send({ type: 'custom', event: 'special-update', data: {} }) return [] }
客户端代码应使用 HMR API 注册相应的处理程序(这可以由同一个插件的
transform
钩子注入)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 构建后插件(缩小、清单、报告)
请注意,这与钩子排序是分开的,钩子排序仍然分别受其 order
属性 如 Rollup 钩子通常一样 的约束。
条件应用
默认情况下,插件会在服务和构建期间调用。在插件需要有条件地仅在服务或构建期间应用的情况下,请使用 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
),但并非所有插件都可以,因为某些插件钩子在非捆绑的开发服务器上下文中没有意义。
一般来说,只要 Rollup 插件符合以下标准,它就应该可以直接用作 Vite 插件
- 它不使用
moduleParsed
钩子。 - 它在捆绑阶段钩子和输出阶段钩子之间没有强耦合。
如果 Rollup 插件仅对构建阶段有意义,则可以在 build.rollupOptions.plugins
下指定它。它的工作原理与具有 enforce: 'post'
和 apply: 'build'
的 Vite 插件相同。
您还可以使用 Vite 特定的属性来增强现有的 Rollup 插件
// vite.config.js
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'
过滤、include/exclude 模式
Vite 公开了 @rollup/pluginutils
的 createFilter
函数,以鼓励 Vite 特定插件和集成使用标准的 include/exclude 过滤模式,该模式也在 Vite 核心本身中使用。
客户端-服务器通信
从 Vite 2.9 开始,我们为插件提供了一些实用程序来帮助处理与客户端的通信。
服务器到客户端
在插件方面,我们可以使用 server.hot.send
(从 Vite 5.1 开始)或 server.ws.send
向所有客户端广播事件
// vite.config.js
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
// Example: wait for a client to connect before sending a message
server.hot.on('connection', () => {
server.hot.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.hot.on
(从 Vite 5.1 开始)或 server.ws.on
,并在服务器端监听事件
// vite.config.js
export default defineConfig({
plugins: [
{
// ...
configureServer(server) {
server.hot.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
可以通过扩展 CustomEventMap
接口来对自定义事件进行类型化
// events.d.ts
import 'vite/types/customEvent'
declare module 'vite/types/customEvent' {
interface CustomEventMap {
'custom:foo': { msg: string }
// 'event-key': payload
}
}