插件 API
Vite 插件扩展了设计出色的 Rollup 接口,带有一些 Vite 独有的配置项。因此,你只需要编写一个 Vite 插件,就可以同时为开发环境和生产环境工作。
推荐在阅读下面的章节之前,首先阅读下 Rollup 插件文档
约定
如果插件不使用 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 {
plugins: [vitePlugin(), rollupPlugin()]
}
假值的插件将被忽略,可以用来轻松地启用或停用插件。
plugins
也可以接受将多个插件作为单个元素的预设。这对于使用多个插件实现的复杂特性(如框架集成)很有用。该数组将在内部被扁平化(flatten)。
// 框架插件
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 framework from 'vite-plugin-framework'
export default {
plugins: [framework()]
}
简单示例
:::tip 通常的惯例是创建一个 Vite/Rollup 插件作为一个返回实际插件对象的工厂函数。该函数可以接受允许用户自定义插件行为的选项。 :::
引入一个虚拟文件
export default function myPlugin() {
const virtualFileId = '@my-virtual-file'
return {
name: 'my-plugin', // 必须的,将会在 warning 和 error 中显示
resolveId(id) {
if (id === virtualFileId) {
return virtualFileId
}
},
load(id) {
if (id === virtualFileId) {
return `export const msg = "from virtual file"`
}
}
}
}
这使得可以在 JavaScript 中引入这些文件:
import { msg } from '@my-virtual-file'
console.log(msg)
转换自定义文件类型
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 // 如果可行将提供 source map
}
}
}
}
}
通用钩子
在开发中,Vite 开发服务器会创建一个插件容器来调用 Rollup 构建钩子,与 Rollup 如出一辙。
以下钩子在服务器启动时被调用:
以下钩子会在每个传入模块请求时被调用:
以下钩子在服务器关闭时被调用:
请注意 moduleParsed
钩子在开发中是 不会 被调用的,因为 Vite 为了性能会避免完整的 AST 解析。
Output Generation Hooks(除了 closeBundle
) 在开发中是 不会 被调用的。你可以认为 Vite 的开发服务器只调用了 rollup.rollup()
而没有调用 bundle.generate()
。
Vite 独有钩子
Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子会被 Rollup 忽略。
config
- 类型:
(config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void
种类:
sync
,sequential
在解析 Vite 配置前调用。钩子接收原始用户配置(命令行选项指定的会与配置文件合并)和一个描述配置环境的变量,包含正在使用的
mode
和command
。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置(如果默认的合并不能达到预期的结果)。示例:
// 返回部分配置(推荐)
const partialConfigPlugin = () => ({
name: 'return-partial',
config: () => ({
alias: {
foo: 'bar'
}
})
})
// 直接改变配置(应仅在合并不起作用时使用)
const mutateConfigPlugin = () => ({
name: 'mutate-config',
config(config, { command }) {
if (command === 'build') {
config.root = __dirname
}
}
})
::: warning 注意 用户插件在运行这个钩子之前会被解析,因此在
config
钩子中注入其他插件不会有任何效果。 :::
configResolved
- 类型:
(config: ResolvedConfig) => void | Promise<void>
种类:
async
,parallel
在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用。
示例:
const exmaplePlugin = () => {
let config
return {
name: 'read-config',
configResolved(resolvedConfig) {
// 存储最终解析的配置
config = resolvedConfig
},
// 在其他钩子中使用存储的配置
transform(code, id) {
if (config.command === 'serve') {
// serve: 由开发服务器调用的插件
} else {
// build: 由 Rollup 调用的插件
}
}
}
}
configureServer
- 类型:
(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>
- 种类:
async
,sequential
此外请看 ViteDevServer
是用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件:
const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定义请求处理...
})
}
})
注入后置中间件
configureServer
钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行。如果你想注入一个在内部中间件 之后 运行的中间件,你可以从configureServer
返回一个函数,将会在内部中间件安装后被调用:const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
// 返回一个在内部中间件安装后
// 被调用的后置钩子
return () => {
server.middlewares.use((req, res, next) => {
// 自定义请求处理...
})
}
}
})
存储服务器访问
在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 websocket 服务器、文件系统监视程序或模块图)。这个钩子也可以用来存储服务器实例以供其他钩子访问:
const myPlugin = () => {
let server
return {
name: 'configure-server',
configureServer(_server) {
server = _server
},
transform(code, id) {
if (server) {
// 使用 server...
}
}
}
}
注意
configureServer
在运行生产版本时不会被调用,所以其他钩子需要防范它缺失。
transformIndexHtml
- 类型:
IndexHtmlTransformHook | { enforce?: 'pre' | 'post' transform: IndexHtmlTransformHook }
种类:
async
,sequential
转换
index.html
的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer
实例,在构建期间暴露 Rollup 输出的包。这个钩子可以是异步的,并且可以返回以下其中之一:
- 经过转换的 HTML 字符串
- 注入到现有 HTML 中的标签描述符对象数组(
{ tag, attrs, children }
)。每个标签也可以指定它应该被注入到哪里(默认是在<head>
之前) - 一个包含
{ html, tags }
的对象
基础示例:
const htmlPlugin = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return html.replace(
/<title>(.*?)<\/title>/,
`<title>Title replaced!</title>`
)
}
}
}
完整钩子签名:
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
attrs?: Record<string, string>
children?: string | HtmlTagDescriptor[]
/**
* 默认: 'head-prepend'
*/
injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
}
handleHotUpdate
类型:
(ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
执行自定义 HMR 更新处理。钩子接收一个带有以下签名的上下文对象:
interface HmrContext {
file: string
timestamp: number
modules: Array<ModuleNode>
read: () => string | Promise<string>
server: ViteDevServer
}
modules
是受更改文件影响的模块数组。它是一个数组,因为单个文件可能映射到多个服务模块(例如 Vue 单文件组件)。read
这是一个异步读函数,它返回文件的内容。之所以这样做,是因为在某些系统上,文件更改的回调函数可能会在编辑器完成文件更新之前过快地触发,并fs.readFile
直接会返回空内容。传入的read
函数规范了这种行为。
钩子可以选择:
过滤和缩小受影响的模块列表,使 HMR 更准确。
返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理:
handleHotUpdate({ server }) {
server.ws.send({
type: 'custom',
event: 'special-update',
data: {}
})
return []
}
客户端代码应该使用 HMR API 注册相应的处理器(这应该被相同插件的
transform
钩子注入):if (import.meta.hot) {
import.meta.hot.on('special-update', (data) => {
// 执行自定义更新
})
}
插件顺序
一个 Vite 插件可以额外指定一个 enforce
属性(类似于 webpack 加载器)来调整它的应用顺序。enforce
的值可以是pre
或 post
。解析后的插件将按照以下顺序排列:
- Alias
- 带有
enforce: 'pre'
的用户插件 - Vite 核心插件
- 没有 enforce 值的用户插件
- Vite 构建用的插件
- 带有
enforce: 'post'
的用户插件 - Vite 后置构建插件(最小化,manifest,报告)
情景应用
默认情况下插件在开发(serve)和构建(build)模式中都会调用。如果插件只需要在预览或构建期间有条件地应用,请使用 apply
属性指明它们仅在 'build'
或 'serve'
模式时调用:
function myPlugin() {
return {
name: 'build-only',
apply: 'build' // 或 'serve'
}
}
Rollup 插件兼容性
相当数量的 Rollup 插件将直接作为 Vite 插件工作(例如:@rollup/plugin-alias
或 @rollup/plugin-json
),但并不是所有的,因为有些插件钩子在非构建式的开发服务器上下文中没有意义。
一般来说,只要 Rollup 插件符合以下标准,它就应该像 Vite 插件一样工作:
- 没有使用
moduleParsed
钩子。 - 它在打包钩子和输出钩子之间没有很强的耦合。
如果一个 Rollup 插件只在构建阶段有意义,则在 build.rollupOptions.plugins
下指定即可。
你也可以用 Vite 独有的属性来扩展现有的 Rollup 插件:
// vite.config.js
import example from 'rollup-plugin-example'
export default {
plugins: [
{
...example(),
enforce: 'post',
apply: 'build'
}
]
}
查看 Vite Rollup 插件 获取兼容的官方 Rollup 插件列表及其使用指南。
路径规范化
Vite 对路径进行了规范化处理,在解析路径时使用 POSIX 分隔符( / ),同时保留了 Windows 中的卷名。而另一方面,Rollup 在默认情况下保持解析的路径不变,因此解析的路径在 Windows 中会使用 win32 分隔符( \ )。然而,Rollup 插件会使用 @rollup/pluginutils
内部的 normalizePath
工具函数,它在执行比较之前将分隔符转换为 POSIX。所以意味着当这些插件在 Vite 中使用时,include
和 exclude
两个配置模式,以及与已解析路径比较相似的路径会正常工作。
所以对于 Vite 插件来说,在将路径与已解析的路径进行比较时,首先规范化路径以使用 POSIX 分隔符是很重要的。从 vite
模块中也导出了一个等效的 normalizePath
工具函数。
import { normalizePath } from 'vite'
normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'