Vite 插件开发实战:从 transform 到 generateBundle 的生命周期掌控
Vite 插件开发实战从 transform 到 generateBundle 的生命周期掌控一、Vite 插件的本质对构建管线的生命周期注入Vite 构建管线是一个高度有序的流水线。从源代码解析到最终产物输出每个阶段都有明确的输入、处理和输出。Vite 插件系统本质上是对这条管线的钩子注入——在特定的生命周期节点插入自定义逻辑实现对构建过程的精确控制。与 Webpack Loader/Plugin 体系不同Vite 的插件模型基于 Rollup 的插件 API 扩展而来同时兼容 Rollup 生态的大量插件。理解这一架构差异是高质量插件开发的前提。flowchart TD A[源代码] -- B[config 钩子] B -- C[configResolved 钩子] C -- D[options 钩子] D -- E[buildStart 钩子] E -- F{模块类型判断} F --|非 JS/TS| G[transform 钩子] F --|JS/TS| H[resolveId 钩子] H -- I[load 钩子] I -- G G -- J[moduleParsed 钩子] J -- K{是否还有未处理模块?} K --|是| F K --|否| L[buildEnd 钩子] L -- M[renderChunk 钩子] M -- N[generateBundle 钩子] N -- O[writeBundle 钩子] O -- P[closeBundle 钩子] P -- Q[构建产物]Vite 插件的生命周期可以分为三个大的阶段构建启动阶段config → buildStart、模块处理阶段resolveId → load → transform、产物生成阶段renderChunk → generateBundle → writeBundle。每个阶段的钩子有特定的调用时机和可用上下文这是插件开发必须掌握的核心知识。二、transform 钩子模块级代码转换的关键节点transform 是使用频率最高的钩子之一。它在每个模块的源代码加载之后、AST 解析之前被调用允许插件对模块内容进行任意转换。典型应用场景包括环境变量注入、条件编译、代码插桩、自定义语法转换。transform 的执行时序import type { Plugin } from vite; /** * transform 钩子的核心签名 * transform(code: string, id: string, options?: { ssr?: boolean }) * TransformResult | PromiseTransformResult * * code: 模块的原始源代码字符串 * id: 模块的绝对路径浏览器环境下为虚拟模块的标识符 * options.ssr: 是否为 SSR 构建 * * 返回值可以是 * - 包含 code 和 map 的对象转换后代码 sourcemap * - null/undefined跳过转换 * - 纯字符串仅代码无 sourcemap */ function transformExamplePlugin(): Plugin { return { name: vite-plugin-transform-example, enforce: pre, // 在核心插件之前执行确保最先处理 transform(code: string, id: string) { // 过滤条件仅处理指定目录下的 TypeScript 文件 if (!id.includes(/src/) || !id.endsWith(.ts)) { return null; // 返回 null 表示不处理由后续插件继续 } // 示例 1移除开发环境下的 console.log // 通过正则替换在构建阶段剔除调试代码 if (process.env.NODE_ENV production) { const stripped code.replace( /console\.(log|debug|info)\(.*?\);?\s*\n?/g, // [stripped] console call removed by transform plugin\n ); return { code: stripped, // 必须提供 map 以保持调试能力 // 实际项目中应使用 magic-string 生成精确的 sourcemap map: null, }; } // 示例 2环境变量编译时替换 // 将 __BUILD_TIME__ 占位符替换为构建时间戳 const buildTime new Date().toISOString(); const injected code.replace( /__BUILD_TIME__/g, JSON.stringify(buildTime) ); return { code: injected, map: null, }; }, }; }多个 transform 插件的执行顺序Vite 会按照enforce属性的值分组执行 transform 钩子enforce: pre的插件在核心插件之前核心插件Vite 内置的 transform普通插件没设置 enforce 或 enforce 为 undefinedenforce: post的插件在核心插件之后在同一 enforce 组内插件按照在plugins数组中的顺序执行。这意味着后一个插件的 transform 接收的 code 是前一个插件的输出。生产级插件示例自定义条件编译import { createFilter } from rollup/pluginutils; import type { Plugin, ResolvedConfig } from vite; // 实际项目中应使用 magic-string 代替字符串操作 // import MagicString from magic-string; interface ConditionalCompileOptions { include?: string | RegExp | (string | RegExp)[]; exclude?: string | RegExp | (string | RegExp)[]; // 功能开关映射key 为开关名value 为是否启用 features: Recordstring, boolean; // 条件编译的指令语法默认使用 if/endif directives?: { ifStart: string; ifEnd: string; elseBlock: string; }; } /** * 自定义条件编译插件 * 支持在源代码中使用指令语法控制代码块的编译时包含/排除 * * 使用示例源代码 * // if FEATURE_EXPERIMENTAL * import { experimentalAPI } from ./experimental; * // endif * * 当 features.FEATURE_EXPERIMENTAL 为 false 时上述 import 会被移除 */ function conditionalCompilePlugin(options: ConditionalCompileOptions): Plugin { const { include /\.(ts|tsx|js|jsx)$/, exclude /node_modules/, features {}, directives { ifStart: if, ifEnd: endif, elseBlock: else, }, } options; const filter createFilter(include, exclude); return { name: vite-plugin-conditional-compile, enforce: pre, transform(code: string, id: string) { if (!filter(id)) return null; // 构建正则表达式以匹配条件编译块 // 匹配// if FEATURE_NAME\n ... \n// endif const ifRegex new RegExp( \\/\\/\\s*${escapeRegex(directives.ifStart)}\\s(\\w)\\s*\\n ([\\s\\S]*?) \\/\\/\\s*${escapeRegex(directives.ifEnd)}, g ); let transformed code; let hasChanges false; transformed transformed.replace(ifRegex, (_match, featureName, block) { hasChanges true; // 处理 else 分支 const elseRegex new RegExp( ([\\s\\S]*?)\\/\\/\\s*${escapeRegex(directives.elseBlock)}\\s*\\n([\\s\\S]*) ); const elseMatch block.match(elseRegex); if (elseMatch) { // 有 else 分支根据开关状态选择对应的代码块 const [, ifBlock, elseBlock] elseMatch; return features[featureName] ? ifBlock : elseBlock; } // 无 else 分支开关为 true 保留代码为 false 移除 return features[featureName] ? block : ; }); if (!hasChanges) return null; return { code: transformed, map: { mappings: }, // 实际项目需生成正确的 sourcemap }; }, }; } // 转义正则表达式特殊字符 function escapeRegex(str: string): string { return str.replace(/[.*?^${}()|[\]\\]/g, \\$); }三、generateBundle产物级别处理的强大钩子generateBundle 在资源生成阶段被调用此时所有模块已被处理完毕chunk 和 asset 已生成但尚未写入磁盘。这个钩子提供了对最终产物的完全控制能力。钩子签名与调用时机import type { Plugin, OutputBundle, OutputChunk, OutputAsset } from vite; /** * generateBundle 钩子签名 * generateBundle( * options: NormalizedOutputOptions, * bundle: OutputBundle, * isWrite: boolean * ) void | Promisevoid * * options: 输出配置format、dir、entryFileNames 等 * bundle: 产物集合Map文件名, OutputChunk | OutputAsset * - OutputChunk: JS/CSS chunk包含 code、map、modules 等字段 * - OutputAsset: 其他资源文件包含 source、fileName 等字段 * isWrite: 是否为写入模式write 为 truegenerate 为 false */ function bundleAnalysisPlugin(): Plugin { return { name: vite-plugin-bundle-analysis, enforce: post, // 在最后执行确保所有插件已完成处理 generateBundle(_options, bundle) { let totalSize 0; const chunks: { name: string; size: number; type: string }[] []; // 遍历产物集合区分 chunk 和 asset for (const [fileName, chunkOrAsset] of Object.entries(bundle)) { if (chunkOrAsset.type chunk) { const chunk chunkOrAsset as OutputChunk; const size Buffer.byteLength(chunk.code, utf-8); totalSize size; // 判断 chunk 是否为入口模块 const type chunk.isEntry ? entry : chunk.isDynamicEntry ? dynamic : chunk; chunks.push({ name: fileName, size, type }); } else { const asset chunkOrAsset as OutputAsset; const size Buffer.byteLength( typeof asset.source string ? asset.source : asset.source.toString(), utf-8 ); totalSize size; chunks.push({ name: fileName, size, type: asset }); } } // 按大小降序排列前 10 个最大的产物 const top10 chunks.sort((a, b) b.size - a.size).slice(0, 10); // 生成分析报告 const report { buildTime: new Date().toISOString(), totalSize: ${(totalSize / 1024).toFixed(2)} KB, totalChunks: chunks.length, top10: top10.map((c) ({ ...c, size: ${(c.size / 1024).toFixed(2)} KB, // 计算该 chunk 占总体的百分比 percentage: ${((c.size / totalSize) * 100).toFixed(1)}%, })), }; console.log(\n[Bundle Analysis] ); console.log(JSON.stringify(report, null, 2)); console.log([Bundle Analysis] \n); }, }; }generateBundle 的核心能力与 transform 只能操作单个模块不同generateBundle 可以做三件事删除或替换整个 chunk、向 bundle 中注入新的资源文件、收集所有模块的依赖关系用于分析。sequenceDiagram participant Plugin as 自定义插件 participant Bundle as OutputBundle participant Chunks as Chunks/Assets participant Disk as 磁盘 Plugin-Bundle: 遍历所有产物 Bundle--Plugin: 返回 OutputChunk | OutputAsset Plugin-Plugin: 决策保留/修改/删除/新增 alt 需要删除 Plugin-Bundle: delete bundle[fileName] else 需要修改 Plugin-Chunks: 直接修改 chunk.code 或 asset.source else 需要新增 Plugin-Bundle: this.emitFile({ type: asset, ... }) end Note over Plugin,Disk: generateBundle 执行完毕 Plugin-Disk: Vite 自动写入 bundle 到磁盘实用案例自动注入构建信息文件import type { Plugin } from vite; /** * 构建元信息注入插件 * 在 generateBundle 阶段生成 build-info.json包含构建时间、版本、Git 信息 * 该文件可用于前端运行时进行版本校验和灰度发布判断 */ function buildInfoPlugin(): Plugin { return { name: vite-plugin-build-info, generateBundle(_options, bundle) { // 收集构建元信息 const buildInfo { buildTime: new Date().toISOString(), // 尝试获取 Git commit hash生产环境 gitCommit: process.env.GIT_COMMIT ?? unknown, // CI 构建编号 buildNumber: process.env.BUILD_NUMBER ?? local, // Node 版本 nodeVersion: process.version, // 入口 chunk 列表 entries: Object.entries(bundle) .filter(([, chunk]) chunk.type chunk chunk.isEntry) .map(([fileName]) fileName), }; // 通过 this.emitFile 向 bundle 中添加自定义资源 // 注意this.emitFile 返回的是引用 ID不会在 bundle 中直接出现 // 需要通过以下方式添加到 bundle const infoFileName build-info.json; const infoContent JSON.stringify(buildInfo, null, 2); // 使用 setAssetSource 等效于 directly add to bundle bundle[infoFileName] { type: asset, fileName: infoFileName, source: infoContent, name: Build Info, needsCodeReference: false, }; // 同时注入到每个入口 chunk 的顶部使运行时可以快速读取 for (const [, chunk] of Object.entries(bundle)) { if (chunk.type chunk chunk.isEntry) { // 在入口 chunk 顶部注入构建信息的全局声明 const preamble // Auto-generated build info // Build Time: ${buildInfo.buildTime} // Git Commit: ${buildInfo.gitCommit} // Build: #${buildInfo.buildNumber} // window.__BUILD_INFO__ ${JSON.stringify(buildInfo)}; ; chunk.code preamble chunk.code; } } }, }; }四、插件开发的高级模式与性能考量插件间的数据传递在复杂的构建流程中多个插件可能需要共享数据。Vite 的插件上下文提供了resolveId返回的meta对象和 Rollup 的moduleParsed钩子中的meta字段用于在插件间传递自定义数据。import type { Plugin } from vite; interface CollectionData { usedModules: Setstring; cssModules: Mapstring, string[]; } /** * 模块使用分析插件对 * 第一个插件在 transform 阶段收集 CSS Module 引用信息 * 第二个插件在 generateBundle 阶段汇总并生成报告 */ function collectPlugin(): Plugin { const KEY module-collector-data; return { name: vite-plugin-collector, enforce: pre, transform(code: string, id: string) { // 收集 CSS Module 导入信息 const cssModuleRegex /import\s(\w)\sfrom\s[](.\.module\.css)[]/g; let match: RegExpExecArray | null; while ((match cssModuleRegex.exec(code)) ! null) { // 由于 transform 钩子之间数据传递机制限制 // 此处通过闭包变量或外部存储实现跨钩子通信 const [, localName, modulePath] match; // 需要解析为绝对路径后存储 console.log([collector] 发现 CSS Module: ${localName} - ${modulePath}); } return null; // 不修改代码仅做收集 }, }; }缓存与性能优化插件的 transform 钩子在开发模式下会被频繁调用每次文件变更触发 HMR 时。对于计算密集型转换如图片压缩、语法树转换缓存机制至关重要。import type { Plugin } from vite; import { createHash } from node:crypto; /** * 带缓存的 transform 插件基类 * 使用内容哈希作为缓存键避免重复处理相同内容 * 仅适用于纯函数式的代码转换输出仅取决于输入 */ function createCachedTransformPlugin( transformFn: (code: string, id: string) string ): Plugin { // 缓存结构哈希值, 转换结果 const cache new Mapstring, string(); return { name: vite-plugin-cached-transform, transform(code: string, id: string) { // 排除 node_modules 中的文件 if (id.includes(node_modules)) return null; // 计算内容 文件路径的联合哈希 const hash createHash(sha256) .update(code) .update(id) .digest(hex) .slice(0, 16); // 取前 16 位即可 // 缓存命中直接返回跳过计算 const cached cache.get(hash); if (cached ! undefined) { return { code: cached, map: null }; } // 缓存未命中执行转换并存入缓存 const transformed transformFn(code, id); cache.set(hash, transformed); // 限制缓存大小防止内存泄漏 if (cache.size 1000) { const firstKey cache.keys().next().value; if (firstKey ! undefined) { cache.delete(firstKey); } } return { code: transformed, map: null }; }, // 构建结束时清理缓存 buildEnd() { cache.clear(); }, }; }五、总结Vite 插件开发的核心在于理解构建生命周期中各个钩子的调用时序和职责边界。transform 钩子处理模块级别的代码转换适合做代码注入、移除和语法转换。generateBundle 钩子处理产物级别的聚合操作适合做分析报告、元信息注入和产物优化。两者配合使用可以覆盖绝大部分的自定义构建需求。插件开发中需要特别注意三点enforce属性决定插件间的执行优先级错误的 enforce 设置会导致转换结果被后续插件覆盖sourcemap 的正确生成是调试能力的基础不能为图省事而传 null缓存机制在生产级插件中不可或缺直接关系到开发体验和构建速度。