一个简单的插件示例
Vite 插件与 Rollup 插件结构类似,为一个name
和各种插件 Hook 的对象:
{
// 插件名称
name: 'vite-plugin-xxx',
load(code) {
// 钩子逻辑
},
}
如果插件是一个 npm 包,在
package.json
中的包命名也推荐以vite-plugin
开头
一般情况下因为要考虑到外部传参,我们不会直接写一个对象,而是实现一个返回插件对象的工厂函数
,如下代码所示:
// myPlugin.js
export function myVitePlugin(options) {
console.log(options)
return {
name: 'vite-plugin-xxx',
load(id) {
// 在钩子逻辑中可以通过闭包访问外部的 options 传参
}
}
}
// 使用方式
// vite.config.ts
import { myVitePlugin } from './myVitePlugin';
export default {
plugins: [myVitePlugin({ /* 给插件传参 */ })]
}
插件 Hook 介绍
1. 通用 Hook
在双引擎架构这一节中介绍过,Vite 开发阶段会模拟 Rollup 的行为
其中 Vite 会调用一系列与 Rollup 兼容的钩子,这个钩子主要分为三个阶段:
- 服务器启动阶段:
options
和buildStart
钩子会在服务启动时被调用。 - 请求响应阶段: 当浏览器发起请求时,Vite 内部依次调用
resolveId
、load
和transform
钩子。 - 服务器关闭阶段: Vite 会依次执行
buildEnd
和closeBundle
钩子。
除了以上钩子,其他 Rollup 插件钩子(如moduleParsed
、renderChunk
)均不会在 Vite 开发阶段调用。而生产环境下,由于 Vite 直接使用 Rollup,Vite 插件中所有 Rollup 的插件钩子都会生效。
2. 独有 Hook
接下来给大家介绍 Vite 中特有的一些 Hook,这些 Hook 只会在 Vite 内部调用,而放到 Rollup 中会被直接忽略。
2.1 给配置再加点料: config
Vite 在读取完配置文件(即vite.config.ts
)之后,会拿到用户导出的配置对象,然后执行 config 钩子。在这个钩子里面,你可以对配置文件导出的对象进行自定义的操作,如下代码所示:
// 返回部分配置(推荐)
const editConfigPlugin = () => ({
name: 'vite-plugin-modify-config',
config: () => ({
alias: {
react: require.resolve('react')
}
})
})
官方推荐的姿势是在 config 钩子中返回一个配置对象,这个配置对象会和 Vite 已有的配置进行深度的合并。不过你也可以通过钩子的入参拿到 config 对象进行自定义的修改,如下代码所示:
代码语言:typescript复制const mutateConfigPlugin = () => ({
name: 'mutate-config',
// command 为 `serve`(开发环境) 或者 `build`(生产环境)
config(config, { command }) {
// 生产环境中修改 root 参数
if (command === 'build') {
config.root = __dirname;
}
}
})
在一些比较深层的对象配置中,这种直接修改配置的方式会显得比较麻烦,如 optimizeDeps.esbuildOptions.plugins
,需要写很多的样板代码,类似下面这样:
// 防止出现 undefined 的情况
config.optimizeDeps = config.optimizeDeps || {}
config.optimizeDeps.esbuildOptions = config.optimizeDeps.esbuildOptions || {}
config.optimizeDeps.esbuildOptions.plugins = config.optimizeDeps.esbuildOptions.plugins || []
因此这种情况下,建议直接返回一个配置对象,这样会方便很多:
代码语言:typescript复制config() {
return {
optimizeDeps: {
esbuildOptions: {
plugins: []
}
}
}
}
2.2 记录最终配置: configResolved
Vite 在解析完配置之后会调用configResolved
钩子,这个钩子一般用来记录最终的配置信息,而不建议再修改配置,用法如下图所示:
const exmaplePlugin = () => {
let config
return {
name: 'read-config',
configResolved(resolvedConfig) {
// 记录最终配置
config = resolvedConfig
},
// 在其他钩子中可以访问到配置
transform(code, id) {
console.log(config)
}
}
}
2.3 获取 Dev Server 实例: configureServer
这个钩子仅在开发阶段会被调用,用于扩展 Vite 的 Dev Server,一般用于增加自定义 server 中间件,如下代码所示:
代码语言:typescript复制const myPlugin = () => ({
name: 'configure-server',
configureServer(server) {
// 姿势 1: 在 Vite 内置中间件之前执行
server.middlewares.use((req, res, next) => {
// 自定义请求处理逻辑
})
// 姿势 2: 在 Vite 内置中间件之后执行
return () => {
server.middlewares.use((req, res, next) => {
// 自定义请求处理逻辑
})
}
}
})
2.4 转换 HTML 内容: transformIndexHtml
这个钩子用来灵活控制 HTML 的内容,你可以拿到原始的 html 内容后进行任意的转换:
代码语言:typescript复制const htmlPlugin = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return html.replace(
/<title>(.*?)</title>/,
`<title>换了个标题</title>`
)
}
}
}
// 也可以返回如下的对象结构,一般用于添加某些标签
const htmlPlugin = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
return {
html,
// 注入标签
tags: [
{
// 放到 body 末尾,可取值还有`head`|`head-prepend`|`body-prepend`,顾名思义
injectTo: 'body',
// 标签属性定义
attrs: { type: 'module', src: './index.ts' },
// 标签名
tag: 'script',
},
],
}
}
}
}
2.5 热更新处理: handleHotUpdate
关于热更新的概念和原理,我们会在下一节具体讲解。
这个钩子会在 Vite 服务端处理热更新时被调用,你可以在这个钩子中拿到热更新相关的上下文信息,进行热更模块的过滤,或者进行自定义的热更处理。下面是一个简单的例子:
代码语言:typescript复制const handleHmrPlugin = () => {
return {
async handleHotUpdate(ctx) {
// 需要热更的文件
console.log(ctx.file)
// 需要热更的模块,如一个 Vue 单文件会涉及多个模块
console.log(ctx.modules)
// 时间戳
console.log(ctx.timestamp)
// Vite Dev Server 实例
console.log(ctx.server)
// 读取最新的文件内容
console.log(await read())
// 自行处理 HMR 事件
ctx.server.ws.send({
type: 'custom',
event: 'special-update',
data: { a: 1 }
})
return []
}
}
}
// 前端代码中加入
if (import.meta.hot) {
import.meta.hot.on('special-update', (data) => {
// 执行自定义更新
// { a: 1 }
console.log(data)
window.location.reload();
})
}
以上就是 Vite 独有的五个钩子,我们来重新梳理一下:
config
: 用来进一步修改配置。configResolved
: 用来记录最终的配置信息。configureServer
: 用来获取 Vite Dev Server 实例,添加中间件。transformIndexHtml
: 用来转换 HTML 的内容。handleHotUpdate
: 用来进行热更新模块的过滤,或者进行自定义的热更新处理。
3. 插件 Hook 执行顺序
好,现在我们学习到了 Vite 的通用钩子和独有钩子,估计你现在脑子里面一点乱: 这么多的钩子,到底谁先执行、谁后执行呢?
下面,我们就来复盘一下上述的两类钩子,并且通过一个具体的代码示例来汇总一下所有的钩子。我们可以在 Vite 的脚手架工程中新建 test-hooks-plugin.ts
:
// test-hooks-plugin.ts
// 注: 请求响应阶段的钩子
// 如 resolveId, load, transform, transformIndexHtml在下文介绍
// 以下为服务启动和关闭的钩子
export default function testHookPlugin () {
return {
name: 'test-hooks-plugin',
// Vite 独有钩子
config(config) {
console.log('config');
},
// Vite 独有钩子
configResolved(resolvedCofnig) {
console.log('configResolved');
},
// 通用钩子
options(opts) {
console.log('options');
return opts;
},
// Vite 独有钩子
configureServer(server) {
console.log('configureServer');
setTimeout(() => {
// 手动退出进程
process.kill(process.pid, 'SIGTERM');
}, 3000)
},
// 通用钩子
buildStart() {
console.log('buildStart');
},
// 通用钩子
buildEnd() {
console.log('buildEnd');
},
// 通用钩子
closeBundle() {
console.log('closeBundle');
}
}
将插件加入到 Vite 配置文件中,然后启动,你可以观察到各个 Hook 的执行顺序
由此我们可以梳理出 Vite 插件的执行顺序
- 服务启动阶段:
config
、configResolved
、options
、configureServer
、buildStart
- 请求响应阶段: 如果是
html
文件,仅执行transformIndexHtml
钩子;对于非 HTML 文件,则依次执行resolveId
、load
和transform
钩子。相信大家学过 Rollup 的插件机制,已经对这三个钩子比较熟悉了。 - 热更新阶段: 执行
handleHotUpdate
钩子。 - 服务关闭阶段: 依次执行
buildEnd
和closeBundle
钩子。
插件应用位置
梳理完 Vite 的各个钩子函数之后,接下来让我们来了解一下 Vite 插件的应用情景和应用顺序。
默认情况下 Vite 插件同时被用于开发环境和生产环境,你可以通过apply
属性来决定应用场景:
{
// 'serve' 表示仅用于开发环境,'build'表示仅用于生产环境
apply: 'serve'
}
apply
参数还可以配置成一个函数,进行更灵活的控制:
// 只用于非 SSR 情况下的生产环境构建
return command === 'build' && !config.build.ssr
}
同时,你也可以通过enforce
属性来指定插件的执行顺序:
{
// 默认为`normal`,可取值还有`pre`和`post`
enforce: 'pre'
}
Vite 会依次执行如下的插件:
- Alias (路径别名)相关的插件。
- ⭐️ 带有
enforce: 'pr
e' 的用户插件。 - Vite 核心插件。
- ⭐️ 没有 enforce 值的用户插件,
也叫普通
插件。 - Vite 生产环境构建用的插件。
- ⭐️ 带有
enforce: 'pos
t' 的用户插件。 - Vite 后置构建插件(如压缩插件)。
插件开发实战
接下来我们进入插件开发的实战环节中,在这个部分我们将一起编写两个 Vite 插件,分别是虚拟模块加载插件
和Svgr 插件
,你将学会从插件开发的常见套路和各种开发技巧。话不多说,让我们现在开始实战吧。
实战案例 1: 虚拟模块加载
首先我们来实现一个虚拟模块的加载插件,可能你会有疑问: 什么是虚拟模块呢?
作为构建工具,一般需要处理两种形式的模块,一种存在于真实的磁盘文件系统中,另一种并不在磁盘而在内存当中,也就是虚拟模块
。通过虚拟模块,我们既可以把自己手写的一些代码字符串作为单独的模块内容,又可以将内存中某些经过计算得出的变量作为模块内容进行加载,非常灵活和方便。接下来让我们通过一些具体的例子来实操一下,首先通过脚手架命令初始化一个react ts
项目:
npm init vite
然后通过pnpm i
安装依赖,接着新建plugins
目录,开始插件的开发:
// plugins/virtual-module.ts
import { Plugin } from 'vite';
// 虚拟模块名称
const virtualFibModuleId = 'virtual:fib';
// Vite 中约定对于虚拟模块,解析后的路径需要加上` `前缀
const resolvedFibVirtualModuleId = '