引言
当 Vite 的配置选项无法满足团队的自定义构建需求时,插件(Plugin) 是唯一的正解。无论是注入构建信息、自动生成路由、解析自定义文件格式,还是拦截与改写模块源码,插件的钩子体系都提供了标准化的接入点。理解插件机制,是「会用 Vite」到「掌控 Vite」的分水岭。
本文从 Vite 插件的双引擎身份讲起——一个插件既是 Rollup 插件(生产构建)又是 Vite 插件(开发服务器),系统覆盖钩子体系、transform/load 的源码转换、虚拟模块、插件顺序与 apply 作用域,最后用三个可直接运行的实战插件示例串联全部概念。
前置:https://plumephp.com/vite-scaffold-engineering/ 与 https://plumephp.com/vite-config-guide/。理解模块图可参考 https://plumephp.com/frontend-vite-deep-dive/。
目录
- 1. 插件是什么:双引擎身份
- 2. 钩子体系全景
- 3. transform 与 load:源码转换
- 4. 虚拟模块:凭空创造的模块
- 5. configureServer:开发期专属能力
- 6. 插件顺序与 apply 作用域
- 7. 实战插件一:注入构建信息
- 8. 实战插件二:解析自定义扩展名
- 9. 实战插件三:虚拟模块暴露配置
- 10. 调试与发布建议
- 11. 总结
- 延伸阅读
1. 插件是什么:双引擎身份
1.1 一个插件,两种运行时
开发模式(vite dev):
Vite 服务器 + esbuild 转译 + 部分 Rollup 钩子(load/transform/resolveId)
生产构建(vite build):
完整 Rollup 管线(esbuild 仅做 minify)
关键结论:插件作者面对的是同一套钩子 API,但必须清楚每个钩子在哪个阶段、哪个引擎下运行。
1.2 最小插件长什么样
import type { Plugin } from 'vite'
export function myPlugin(): Plugin {
return {
name: 'my-plugin', // 必须:用于报错与日志
// 可选钩子
buildStart() {
this.log('构建开始')
},
}
}
1.3 在 vite.config 中启用
import { defineConfig } from 'vite'
import { myPlugin } from './plugins/my-plugin'
export default defineConfig({
plugins: [myPlugin()],
})
2. 钩子体系全景
2.1 按生命周期分组
| 类别 | 钩子 | 用途 |
|---|---|---|
| 解析 | resolveId | 决定模块 ID 如何解析 |
| 加载 | load | 读取/生成模块内容 |
| 转换 | transform | 修改模块源码 |
| 服务端 | configureServer | 修改 dev server(中间件/钩子) |
| 构建产物 | generateBundle / writeBundle | 操作最终输出 |
| 生命周期 | buildStart / buildEnd | 构建开始/结束 |
2.2 开发期 vs 构建期
| 钩子 | dev | build |
|---|---|---|
resolveId | ✅ | ✅ |
load | ✅ | ✅ |
transform | ✅ | ✅ |
configureServer | ✅ | ❌ |
configurePreviewServer | ✅ | ✅(preview) |
generateBundle | ❌ | ✅ |
closeBundle | ❌ | ✅ |
3. transform 与 load:源码转换
3.1 load:提供模块内容
export default {
name: 'virtual-utils',
load(id) {
// 只处理虚拟模块
if (id === 'virtual:utils') {
return `export const now = () => Date.now()`
}
},
}
3.2 transform:改写源码
export default {
name: 'add-version',
transform(code, id) {
// 只处理 src 下的 .ts 文件
if (!id.includes('/src/') || !id.endsWith('.ts')) return
return code.replace(
/__APP_VERSION__/g,
`'${process.env.npm_package_version}'`,
)
},
}
3.3 transform 的返回格式
transform(code, id) {
if (!id.endsWith('.md')) return
return {
code: compiledCode,
map: null, // 需要时提供 sourcemap
}
}
不处理时返回 undefined(Vite 继续走后续插件);返回 null 明确表示不处理。
4. 虚拟模块:凭空创造的模块
4.1 为什么需要虚拟模块
有些数据(配置、文件列表、构建时信息)在运行时并不存在于文件系统,直接用普通 import 会失败。虚拟模块让这些内容以模块形式呈现:
import config from 'virtual:app-config' // 并非真实文件
4.2 实现虚拟模块
export default {
name: 'virtual-config',
resolveId(id) {
if (id === 'virtual:app-config') {
return '\0virtual:app-config' // \0 前缀标记,防止被当作真实路径
}
},
load(id) {
if (id === '\0virtual:app-config') {
return `export default { name: '${process.env.APP_NAME}', version: '1.0.0' }`
}
},
}
4.3 关键约定
\0前缀:让其他插件/工具不会把它误认为磁盘文件。resolveId返回的 ID 必须与load匹配。- 虚拟模块可包含 HMR 边界,配合
import.meta.hot实现热更新。
5. configureServer:开发期专属能力
5.1 中间件注入
import { defineConfig, type Plugin } from 'vite'
function serverLogger(): Plugin {
return {
name: 'server-logger',
configureServer(server) {
// 返回中间件函数,附加在内部中间件之前
return (req, res, next) => {
console.log(`[req] ${req.method} ${req.url}`)
next()
}
},
}
}
5.2 访问 server 实例
configureServer(server) {
// 在模块加载前执行
server.middlewares.use((req, res, next) => {
// ...
next()
})
// 在 Vite 启动后执行
server.httpServer?.once('listening', () => {
console.log('dev server ready')
})
}
5.3 执行时机
configureServer 在内部中间件安装前被调用,因此返回的中间件会先于静态文件与转换中间件执行——适合做访问控制、日志、mock。
6. 插件顺序与 apply 作用域
6.1 执行顺序(同类钩子)
先注册的插件先执行(数组顺序)
别名 alias 插件默认放最前
Vite 核心插件与用户插件分层
6.2 控制执行顺序
export function importantPlugin(): Plugin {
return {
name: 'important',
enforce: 'pre', // pre / normal(默认) / post
transform(code, id) {
// pre 阶段先于其他用户插件执行
},
}
}
enforce | 顺序 |
|---|---|
'pre' | 最早(在别名解析后) |
'normal'(默认) | 中间 |
'post' | 最后(在 Vite 内置转换前) |
6.3 apply:限定运行环境
function ssrOnly(): Plugin {
return {
name: 'ssr-only',
apply: 'build', // 只在 build 时生效
// apply: (config, { command }) => command === 'build'
// apply: 'serve' // 只在 dev 时生效
}
}
7. 实战插件一:注入构建信息
把构建时间与 git commit 注入到 import.meta.env 或全局常量。
import { execSync } from 'node:child_process'
import type { Plugin } from 'vite'
export function buildInfo(): Plugin {
return {
name: 'build-info',
config() {
const time = new Date().toISOString()
let commit = ''
try {
commit = execSync('git rev-parse --short HEAD').toString().trim()
} catch { /* 非 git 仓库 */ }
return {
define: {
__BUILD_TIME__: JSON.stringify(time),
__COMMIT_HASH__: JSON.stringify(commit),
},
}
},
}
}
在代码中使用:
console.log(`构建时间: ${__BUILD_TIME__}`)
console.log(`commit: ${__COMMIT_HASH__}`)
8. 实战插件二:解析自定义扩展名
让 Vite 直接加载 .graphql 文件为请求字符串:
import { readFileSync } from 'node:fs'
import type { Plugin } from 'vite'
export function graphqlLoader(): Plugin {
return {
name: 'graphql-loader',
// 只拦截 .graphql 结尾的模块
resolveId(source, importer) {
if (source.endsWith('.graphql')) {
// 交给 Node 解析真实路径
return null
}
},
load(id) {
if (id.endsWith('.graphql')) {
const content = readFileSync(id, 'utf-8')
return `export default ${JSON.stringify(content)}`
}
},
// 开发期需要 watch 源文件变化
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.graphql')) {
const mod = ctx.modules.find(m => m.file?.endsWith('.graphql'))
if (mod) mod.importers.forEach(i => i.hot.accept())
}
},
}
}
9. 实战插件三:虚拟模块暴露配置
将 vite 配置中的自定义选项暴露为模块,供应用内使用:
import type { Plugin, ResolvedConfig } from 'vite'
interface Options {
appName: string
debug?: boolean
}
export function exposeConfig(opts: Options): Plugin {
let config: ResolvedConfig
const virtualId = 'virtual:app-config'
const resolvedVirtual = `\0${virtualId}`
return {
name: 'expose-config',
configResolved(resolved) {
config = resolved
},
resolveId(id) {
if (id === virtualId) return resolvedVirtual
},
load(id) {
if (id === resolvedVirtual) {
return `
export const appConfig = ${JSON.stringify(opts)}
export const mode = '${config.mode}'
export default appConfig
`
}
},
}
}
// 使用
import appConfig, { mode } from 'virtual:app-config'
console.log(appConfig, mode)
10. 调试与发布建议
10.1 用 this.debug 与插件顺序日志
function debugOrder(): Plugin {
return {
name: 'debug-order',
buildStart() {
this.debug?.('插件启动')
},
transform(code, id) {
console.log(`[transform] ${id}`)
return undefined
},
}
}
10.2 发布前的 Checklist
| 项 | 说明 |
|---|---|
name 唯一且语义化 | 出错日志可读 |
| TypeScript 类型声明 | declare module 暴露 hooks |
处理 \0 前缀 | 虚拟模块安全 |
| 同时考虑 dev/build | 用 apply 区分 |
| 写单元测试 | 用 vite 的 dev/build 在 CI 中验证 |
11. 总结
11.1 核心模型
解析(resolveId) -> 加载(load) -> 转换(transform) -> 产出
└── 虚拟模块让「不存在」的文件可被 import
└── configureServer 注入开发期中间件
11.2 关键要点
- 插件是 Rollup 插件 + Vite 插件的统一封装,钩子按生命周期分组。
transform/load是源码转换的主力,返回 undefined 表示不处理。- 虚拟模块用
\0前缀防误解析,适合暴露构建期数据。 enforce/apply控制顺序与环境,是大型插件库的必修课。
11.3 自检清单
| 检查项 | 是否掌握 |
|---|---|
| 能说明 dev 与 build 的钩子差异 | ☐ |
| 能写 transform 改写模块源码 | ☐ |
| 能实现并导入虚拟模块 | ☐ |
| 能配置 apply 限定运行环境 | ☐ |
| 能独立发布一个 Vite 插件 | ☐ |
延伸阅读
- https://plumephp.com/vite-config-guide/ — 插件在配置中的完整用法
- https://plumephp.com/vite-build-optimization/ — 用插件做构建优化
- https://plumephp.com/frontend-vite-deep-dive/ — 模块图与 HMR 底层原理
- Vite 插件 API 文档 — 官方完整钩子索引
- Rollup 插件开发文档 — 底层打包钩子参考
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。