好的 insomniac!下面用一次到位的方式,把 Vite 的插件机制讲清楚:从“能做什么”、到“生命周期/执行顺序”、到“常用钩子与场景”、再到“几个小而全的示例插件”,最后给你一份最佳实践与避坑清单。读完就能自己写出靠谱的 Vite 插件。
1)Vite 插件能做什么
- 在 开发服务器(serve) 与 打包(build) 两个阶段介入
- 自定义 模块解析(别名/虚拟模块/跨端映射)
- 自定义 加载 & 编译(把任意文件转成 JS/CSS,比如 Markdown/MDX、YAML、SVG、GLSL…)
- 注入/改变 index.html(插入脚本、预加载、meta)
- 参与 HMR(热更新)和 模块图(Module Graph) 的控制
- 扩展 SSR & Preview 行为
- 兼容 Rollup 插件(构建阶段 100% 兼容;开发阶段只支持 Vite 自己的扩展钩子)
2)插件结构与启用方式
// vite.config.ts
import type { Plugin } from 'vite'
function MyPlugin(options?: { /* ... */ }): Plugin {
return {
name: 'my-plugin',
// enforce: 'pre' | 'post', // 可调顺序
// apply: 'serve' | 'build', // 可限定环境
// config / configResolved / configureServer / transformIndexHtml ...
}
}
export default {
plugins: [MyPlugin(/* opts */)]
}- enforce:
- pre:在 Vite 内置核心插件之前运行(适合早期介入解析/替换)
- 默认:普通阶段
- post:在内置插件之后(适合做最终收尾或依赖前序产物)
- apply: 只在 serve(开发)或 build(构建)阶段生效
- 只想影响打包、且是纯 Rollup 能力 → 放到 build.rollupOptions.plugins 即可
3)执行顺序(直观版)
以常见钩子为主的顺序/行为:
开发阶段(dev server)
- config(合并用户配置,早期修改)
- configResolved(拿到最终 config)
- configureServer(拿到 Koa/Connect 中间件,能加自定义路由、代理)
- 模块请求时触发:a. resolveId(解析路径/别名/虚拟模块)b. load(读取文件内容或返回虚拟内容)c. transform(源码转换,如 TS/JSX/自定义语法)
- 首屏 HTML:transformIndexHtml
- 变更时:handleHotUpdate(定制 HMR 行为)
- 退出:buildEnd / closeBundle(dev 里很少用)
构建阶段(Rollup)
- config → configResolved
- buildStart
- 每个模块:resolveId → load → transform
- 打包产出:generateBundle
- 结束:buildEnd → closeBundle
注意:resolveId/load 是“谁先返回谁生效”,transform 是“流水线顺序串联”。
4)常用钩子速查
- config(config, env):在用户配置合并前后做修改(如动态设定 alias、define)
- configResolved(resolvedConfig):拿最终配置(比如拿 isProduction、root、base、logger)
- configureServer(server):仅开发,可 server.middlewares.use() 增加路由、调试端点、返回虚拟文件等
- transformIndexHtml(html|{ html, path }):注入 <script>、<link> 或改写 HTML
- resolveId(source, importer, options):改写模块解析(别名、virtual: 前缀、跨包导入重定向)
- load(id, options):返回源码字符串/源码与 map(虚拟模块或自定义读取)
- transform(code, id, options):将任意文本转 JS/CSS,返回 { code, map }
- handleHotUpdate(ctx):定制 HMR,可返回要重新加载的模块集
- buildStart / buildEnd / generateBundle / closeBundle:构建期收尾、产物清单处理、生成额外文件等
- configurePreviewServer(server):vite preview 阶段注入中间件(与 dev 不同的预览服务器)
5)重要参数与上下文
- 钩子 options.ssr:区分 SSR 与非 SSR(不同处理逻辑)
- 钩子 this 上下文(Rollup 语义):
- this.parse(code):拿 AST
- this.emitFile({ type:'asset'|'chunk', ... }):发射额外文件
- this.addWatchFile(file):让 dev 监听额外文件
- this.getModuleInfo(id):查询模块依赖信息
- this.warn() / this.error():报告
- 路径:用 import { normalizePath } from 'vite' 统一分隔符(Windows 友好)
- 过滤文件:@rollup/pluginutils 的 createFilter(include, exclude) 很实用
6)几个“常见需求 → 最小可用”示例
A. 虚拟模块(virtual module)
// virtual:env 插件 —— 提供一个虚拟模块导出环境变量
import type { Plugin } from 'vite'
import { normalizePath } from 'vite'
export default function VirtualEnv(): Plugin {
const VIRTUAL_ID = 'virtual:env'
const RESOLVED = '\0' + VIRTUAL_ID // Rollup 约定:内部/虚拟用 \0 前缀
return {
name: 'virtual-env',
resolveId(id) {
if (id === VIRTUAL_ID) return RESOLVED
},
load(id) {
if (id === RESOLVED) {
return `export const MODE = ${JSON.stringify(process.env.NODE_ENV)}`
}
}
}
}B. Markdown 作为模块(最常见 transform)
// 把 .md 转成导出 HTML 字符串的模块
import type { Plugin } from 'vite'
import { createFilter } from '@rollup/pluginutils'
import { marked } from 'marked'
export default function MarkdownAsHtml(opts?: { include?: any; exclude?: any }): Plugin {
const filter = createFilter(opts?.include ?? ['**/*.md'], opts?.exclude)
return {
name: 'md-as-html',
async transform(code, id) {
if (!filter(id)) return
const html = marked.parse(code)
return {
code: `export default ${JSON.stringify(html)};`,
map: null
}
}
}
}C. 注入首屏脚本(transformIndexHtml)
// 给 index.html 注入性能监控脚本
export default function InjectPerf(): import('vite').Plugin {
return {
name: 'inject-perf',
transformIndexHtml(html) {
return {
html,
tags: [
{
tag: 'script',
children: `console.log('[perf]', performance.now());`,
injectTo: 'head' // 'head' | 'body' | 'head-prepend' | 'body-prepend'
}
]
}
}
}
}D. HMR 精准更新(handleHotUpdate)
// 当某个外部配置文件变更时,主动触发依赖它的虚拟模块热更新
import fs from 'node:fs'
export default function ExternalConfigHMR(configPath: string) {
return {
name: 'external-config-hmr',
configureServer(server) {
server.watcher.add(configPath) // 让 chokidar 监听
},
handleHotUpdate(ctx) {
if (ctx.file === configPath) {
const mod = ctx.server.moduleGraph.getModuleById('\0virtual:env')
if (mod) return [mod] // 仅更新这个模块,避免整页刷新
}
}
}
}E. 只在构建期生成额外文件(generateBundle)
// 打包后生成 manifest.json 摘要
export default function BuildManifest(): import('vite').Plugin {
return {
name: 'build-manifest',
apply: 'build',
generateBundle(_, bundle) {
const files = Object.keys(bundle)
this.emitFile({
type: 'asset',
fileName: 'assets-summary.json',
source: JSON.stringify({ files }, null, 2)
})
}
}
}7)插件在 SSR/Preview/优化阶段的差异
- SSR:很多钩子会带 options.ssr===true,例如 transform(code, id, { ssr: true }),你可以选择输出与浏览器不同的产物(如去掉 DOM 相关逻辑)
- Preview(vite preview):与 dev 不同,configurePreviewServer 用来给静态预览加中间件
- 依赖预构建(esbuild):如果你要影响 optimizeDeps 阶段(把第三方包预构建),需要在 optimizeDeps.esbuildOptions.plugins 里写 esbuild 插件(与 Vite 插件不同)
8)调试与性能
- 打开日志:vite --debug plugin:my-plugin(或在 config.logger 使用)
- 输出 Source Map:transform 时返回 map,利于调试
- 充分利用 include/exclude 与 早返回 避免无谓开销
- 跨平台路径用 normalizePath;尽量用 path.posix 规范化
- HMR 相关逻辑尽量 最小化更新范围(返回精确模块列表)
9)最佳实践清单
- ✅ 给插件起 唯一且语义明确 的 name
- ✅ 用 apply: 'serve'|'build' 控制生效环境
- ✅ 用 enforce: 'pre'|'post' 明确与其他插件顺序关系(尤其和框架插件如 React/Vue)
- ✅ 使用 createFilter() 做 include/exclude
- ✅ 生成虚拟模块 id 时使用 \0 前缀避免与真实路径冲突
- ✅ SSR 分支:if (options?.ssr) { ... }
- ✅ 在 configureServer 里添加自定义 dev 中间件,而不是劫持外部服务器
- ✅ 在构建阶段产出额外文件使用 emitFile / generateBundle
- ❌ 不要在 transform 里做阻塞 I/O;异步为佳
- ❌ 不要在 dev 阶段依赖只存在于 build 的产物(反之亦然)