如何开发一个 Vite 插件?常用钩子怎么选?
Vite 插件就是一个带 name 和若干钩子的对象。它和 Rollup 插件很像,因为生产构建阶段复用了 Rollup;但 Vite 还有开发服务器、HMR、HTML 转换和依赖预构建,所以写插件不能只按构建阶段思考。
最小插件怎么写
插件通常写成函数,方便接收选项。真正需要哪个钩子就写哪个,不要一开始就把所有钩子列满。
tsimport type { Plugin } from 'vite' export function replaceVersion(version: string): Plugin { return { name: 'replace-version', transform(code, id) { if (!id.endsWith('.ts') && !id.endsWith('.tsx')) return null return code.replace(/__APP_VERSION__/g, JSON.stringify(version)) } } }
transform 很常用,也最容易拖慢项目。每个模块都可能经过它,所以一定要先过滤文件类型和范围。
config 和 configResolved 怎么选
config 适合补默认配置、插入别名、修改构建选项。configResolved 在最终配置确定后执行,适合读取配置、保存状态、输出调试信息,不建议再改配置。
tsexport function inspectMode(): Plugin { let isBuild = false return { name: 'inspect-mode', configResolved(config) { isBuild = config.command === 'build' }, transform(code, id) { if (isBuild && id.endsWith('.js')) return code return null } } }
边界是“改配置”和“读配置”要分开。都塞进 configResolved,后续会遇到插件顺序和覆盖问题。
resolveId 和 load 适合虚拟模块
如果希望用户写 import data from 'virtual:xxx',就需要 resolveId 和 load。前者声明模块由插件接管,后者返回模块代码。
tsexport function virtualBuildTime(): Plugin { const virtualId = 'virtual:build-time' const resolvedId = '\0' + virtualId return { name: 'virtual-build-time', resolveId(id) { if (id === virtualId) return resolvedId }, load(id) { if (id === resolvedId) { return `export const buildTime = ${JSON.stringify(new Date().toISOString())}` } } } }
虚拟模块适合构建时间、自动路由、主题变量、Markdown 索引。内部 ID 加 \0 前缀,可以避免被当成真实文件继续解析。
configureServer 和 handleHotUpdate 只服务开发
configureServer 可以加中间件,适合 mock、调试端点或开发期资源服务。handleHotUpdate 用来控制文件变化后的 HMR 范围。
tsexport function mockHealth(): Plugin { return { name: 'mock-health', apply: 'serve', configureServer(server) { server.middlewares.use('/__health', (_req, res) => res.end('ok')) } } }
这些能力不会出现在生产构建里。要在文档里写清楚,否则使用者可能误以为插件提供了线上接口能力。
插件顺序和阶段要克制
enforce: 'pre' | 'post' 控制相对顺序,apply: 'serve' | 'build' 限制运行阶段。只服务开发的插件写 apply: 'serve',只改产物的插件写 apply: 'build'。
tsexport function devOnlyPlugin(): Plugin { return { name: 'dev-only', apply: 'serve', enforce: 'pre' } }
不要滥用 pre。太早处理文件,可能抢在 Vue、React、Svelte 插件之前,把还没转换的框架文件处理坏。
追问
Vite 插件和 Rollup 插件能完全通用吗?
不能完全通用。生产构建阶段兼容度高,开发阶段则有 dev server、HMR、HTML 入口等 Vite 专属行为。取舍是纯构建类插件尽量按 Rollup 写,开发体验类插件要使用 Vite 钩子。
transform 为什么要先过滤 id?
因为开发时每个请求模块都可能经过 transform,不过滤会处理大量无关文件。小项目不明显,大项目 HMR 会变慢。边界是不要一刀切排除所有依赖,有些 workspace 源码包确实需要被插件处理。
什么时候该用虚拟模块?
当数据没有真实源文件,或不想让业务直接读文件系统时,可以用虚拟模块。比如构建时间、路由清单、主题变量都合适。踩坑点是虚拟模块增加调试成本,所以命名和输出要稳定。
handleHotUpdate 要不要精细控制?
只有理解依赖关系时才精细控制。配置文件影响全局行为时,直接 full reload 往往比错误的局部 HMR 更可靠。取舍是刷新体验和正确性,插件开发里正确性优先。