07 | 插件系统全解析:从 Hook 到 Pipeline 的设计艺术
Vite 深度系列 · 第二阶段 · 技术实现深挖
预计阅读:8 分钟 | 难度:⭐⭐⭐
核心问题:Vite 插件系统如何做到「写一次,开发和构建都能用」?
摘要
Vite 的插件系统是它最精巧的设计之一——兼容 Rollup 插件 API,同时扩展了开发时专有钩子,让一个插件同时服务 Dev 和 Build 两种模式。Vite 8 的 Rolldown 统一引擎更进一步:通过 hookFilter 减少 Rust/JS 通信开销,插件性能提升显著。本文从 PluginContainer 到 HookPipeline,逐层拆解 Vite 插件的执行模型。
1. 插件系统架构总览
1.1 三层架构
┌─────────────────────────────────────────┐
│ Vite Plugin System │
├─────────────────────────────────────────┤
│ PluginContainer │
│ ├── 管理插件注册与排序 │
│ ├── 分发钩子调用 │
│ └── 维护插件上下文 │
├─────────────────────────────────────────┤
│ HookPipeline │
│ ├── Sequential(串行):configResolved │
│ ├── Parallel(并行):load / transform │
│ └── First(首个命中):resolveId │
├─────────────────────────────────────────┤
│ Plugin Execution │
│ ├── Rollup 兼容钩子(7 个) │
│ ├── Vite 独有钩子(5 个) │
│ └── Rolldown 扩展钩子(Vite 8+) │
└─────────────────────────────────────────┘1.2 插件的本质
一个 Vite 插件就是一个对象,包含名字和钩子函数:
export default function myPlugin(options) {
return {
name: 'my-plugin', // 必须唯一
enforce: 'post', // 执行顺序:pre | normal | post
apply: 'serve', // 应用场景:serve | build | (默认两者)
// Rollup 兼容钩子
resolveId(source, importer) { /* ... */ },
load(id) { /* ... */ },
transform(code, id) { /* ... */ },
// Vite 独有钩子
config(config) { /* ... */ },
configureServer(server) { /* ... */ },
handleHotUpdate(ctx) { /* ... */ },
}
}2. Rollup 兼容钩子(7 个核心)
这些钩子来自 Rollup,在 Dev 和 Build 模式下都会被调用:
options | |||
buildStart | |||
resolveId | |||
load | |||
transform | |||
buildEnd | |||
generateBundle |
执行模型详解
First 模型(resolveId / load):
// 遍历所有插件,第一个返回非 null 结果的插件胜出
for (const plugin of plugins) {
const result = plugin.resolveId(source, importer)
if (result != null) return result // 后续插件不再调用
}Sequential 模型(transform):
// 所有插件依次执行,前一个的输出是后一个的输入
let code = originalCode
for (const plugin of plugins) {
const result = plugin.transform(code, id)
if (result) code = result.code // 管线式串联
}3. Vite 独有钩子(5 个)
这些钩子只在开发模式下有效(configureServer 和 handleHotUpdate)或两种模式都有(config):
3.1 config / configResolved
export default function myPlugin() {
return {
name: 'my-plugin',
// 修改用户配置(在配置解析前)
config(config) {
return {
define: { __MY_FLAG__: JSON.stringify(true) },
server: { port: 3000 }
}
},
// 读取最终配置(在配置解析后)
configResolved(resolvedConfig) {
console.log('最终端口:', resolvedConfig.server.port)
// 存储供后续钩子使用
this.config = resolvedConfig
}
}
}3.2 configureServer
export default function myPlugin() {
return {
name: 'api-mock-plugin',
configureServer(server) {
// 添加中间件(在 Vite 内置中间件之前)
server.middlewares.use('/api', (req, res) => {
res.end(JSON.stringify({ msg: 'mocked' }))
})
// 或在内部中间件之后
return () => {
server.middlewares.use('/api2', handler)
}
}
}
}3.3 transformIndexHtml
export default function myPlugin() {
return {
name: 'html-transform-plugin',
transformIndexHtml(html) {
return html.replace(
'</head>',
'<script>console.log("injected")</script></head>'
)
}
}
}3.4 handleHotUpdate
export default function myPlugin() {
return {
name: 'hmr-filter-plugin',
handleHotUpdate({ file, server, modules, read }) {
if (file.endsWith('.md')) {
// 自定义 HMR 行为:只重新加载特定模块
const mods = modules.filter(m => m.id?.includes('docs'))
return mods
}
// 返回 undefined 表示使用默认行为
}
}
}4. 插件顺序控制
4.1 enforce 排序
┌─ pre 钩子 ─────────────────────────┐
│ vite:pre-alias, vite:resolve │
├─ normal 钩子 ──────────────────────┤
│ 用户插件(按配置顺序) │
├─ post 钩子 ────────────────────────┤
│ vite:define, vite:css-post │
└─────────────────────────────────────┘4.2 apply 过滤
// 只在开发模式生效
{ name: 'dev-only', apply: 'serve' }
// 只在构建模式生效
{ name: 'build-only', apply: 'build' }
// 条件判断
{ name: 'conditional', apply: (config, { command }) => {
return command === 'build' && config.mode === 'production'
}}5. Vite 8:Rolldown 插件兼容
Vite 8 用 Rolldown 替代了 esbuild + Rollup,但插件 API 保持兼容:
5.1 自动兼容层
// Vite 8 内部自动转换
rollupOptions.output → rolldownOptions.output
esbuild.transform → oxc.transform大多数现有 Vite 插件无需修改即可在 Vite 8 中运行。
5.2 hookFilter(性能优化)
Rust/JS 通信是 Vite 8 的性能瓶颈。hookFilter 让 Rolldown 在 Rust 侧直接过滤,减少不必要的 JS 调用:
export default function myPlugin() {
return {
name: 'optimized-plugin',
transform: {
// 只对 .vue 文件调用 JS 的 transform
filter: { id: /\.vue$/ },
handler(code, id) {
// Rust 侧已过滤,这里只会收到 .vue 文件
return transformVue(code)
}
}
}
}性能影响:大型项目(10k+ 模块)中,hookFilter 可减少 60% 以上的 JS ↔ Rust 通信。
5.3 moduleType 标注
export default function myPlugin() {
return {
name: 'my-plugin',
transform: {
// 告诉 Rolldown 输出类型
moduleType: 'js', // 'js' | 'css' | 'asset'
handler(code, id) {
return { code: transpiled, map: sourcemap }
}
}
}
}6. 插件通信机制
6.1 resolvedConfig 共享
// 插件 A
configResolved(config) {
config.myCustomData = { theme: 'dark' }
}
// 插件 B(在 A 之后执行)
configResolved(config) {
console.log(config.myCustomData.theme) // 'dark'
}6.2 虚拟模块
// 插件 A:提供虚拟模块
resolveId(id) {
if (id === 'virtual:config') return '\0virtual:config'
},
load(id) {
if (id === '\0virtual:config') {
return `export const theme = "${this.config.theme}"`
}
}
// 源码中直接使用
import { theme } from 'virtual:config'6.3 自定义上下文
// Vite 8 的插件上下文扩展
export default function myPlugin() {
return {
name: 'ctx-plugin',
async transform(code, id) {
// this.resolve — 解析模块路径
// this.load — 加载模块
// this.emitFile — 输出文件
// this.addWatchFile — 添加监听
const resolved = await this.resolve('./theme.css', id)
}
}
}7. 钩子执行时序图
一次完整的 Dev 请求处理流程:
请求 /src/App.vue
│
▼
┌─────────┐
│ resolveId │ ← 依次调用所有插件的 resolveId
└────┬─────┘ 返回 /absolute/path/App.vue
│
▼
┌─────────┐
│ load │ ← 依次调用,第一个返回内容的胜出
└────┬─────┘ 返回原始 SFC 代码
│
▼
┌──────────┐
│ transform │ ← 串行执行所有插件
│ pre │ vite:vue → @vitejs/plugin-vue
│ normal │ 用户插件
│ post │ vite:esbuild/oxc → vite:import-analysis
└────┬──────┘ 返回编译后的 JS 代码
│
▼
HTTP 响应给浏览器8. 插件开发 Checklist
\0 | ||
9. 速查卡
┌──────────────────────────────────────────────────────┐
│ Vite 插件钩子速查 │
├──────────────┬──────────┬────────────────────────────┤
│ 钩子 │ 模型 │ 最佳用途 │
├──────────────┼──────────┼────────────────────────────┤
│ config │ Seq │ 修改/合并用户配置 │
│ configResolved│ Seq │ 读取最终配置 │
│ configureServer│ Seq │ 添加开发服务器中间件 │
│ transformIndexHtml│ Seq │ 修改 HTML │
│ resolveId │ First │ 虚拟模块/路径映射 │
│ load │ First │ 自定义加载逻辑 │
│ transform │ Seq │ 代码转换 │
│ handleHotUpdate│ First │ 自定义 HMR │
│ buildStart │ Seq │ 构建初始化 │
│ buildEnd │ Seq │ 构建清理 │
│ generateBundle│ Seq │ 修改产物 │
└──────────────┴──────────┴────────────────────────────┘下期预告
08 | 手把手:5 个实战插件从入门到精通 — 从 Banner 注入到 i18n 提取,5 个真实可用的插件逐行讲解,每个都带完整测试。
💡 移动APP开发 | 资讯·工具·教程·社区
📱 关注我们,获取更多移动开发技术干货
💬 加入社群,与全国开发者交流成长
❤️ 觉得有用?点个"在看"分享给更多人!
夜雨聆风