Vite @vitejs/plugin-legacy 深度实战:Legacy 双份构建、Polyfill 自动按需注入与 CSP 配合
Vite vitejs/plugin-legacy 深度实战Legacy 双份构建、Polyfill 自动按需注入与 CSP 配合【免费下载链接】viteNext generation frontend tooling. Its fast!项目地址: https://gitcode.com/GitHub_Trending/vi/vite本篇围绕vitejs/plugin-legacy展开讲清楚这个插件如何为不支持原生 ESM 特性动态import与import.meta的旧浏览器生成可用的生产构建包括 legacy / modern 双份 chunk 的生成机制、基于 Babel 的语法降级与 SystemJS 模块转换、按“目标浏览器 实际用法”检测的 polyfill 按需注入、HTML 注入顺序以及 CSP 哈希的配合方式。读完你可以直接在生产项目中配置该插件并理解其每个选项在构建管线中真正作用的位置。一、它解决什么问题Vite 的最小浏览器支持目标是原生 ESM 动态import与import.meta。vitejs/plugin-legacy的作用就是在生产构建vite build时为不支持这些特性的旧浏览器提供可用的构建产物。它只在 build 阶段生效开发服务器vite serve不做任何 legacy 处理——从源码看Babel 也是懒加载的// lazy load babel since its not used during dev见 src/index.ts#L31-L35。当前仓库中该插件版本为8.2.3其 package.json 声明enginesnode ^20.19.0 || 22.12.0peerDependenciesterser ^5.16.0与vite ^8.0.0关键运行时依赖babel/core、babel/preset-env、babel/plugin-transform-modules-systemjs、babel-plugin-polyfill-corejs3、babel-plugin-polyfill-regenerator、core-js ^3.50.0、systemjs ^6.15.1、browserslist、browserslist-to-esbuild。另外 package.json 中的compatiblePackages字段标注该插件Only supports Vite即与 rolldown / rollup 生态不兼容这是使用前提。默认行为插件“开箱即用”时做了什么按 README 的说明默认情况下插件会做四件事为最终 bundle 中的每个 chunk 生成对应的 legacy chunk用babel/preset-env做语法降级并输出为SystemJS 模块——代码分割能力仍然保留生成一个 polyfill chunk包含 SystemJS 运行时以及根据“指定浏览器目标 bundle 中实际用法”检测出的必要 polyfills向产物 HTML 注入script nomodule标签只在那些不支持相关特性的浏览器里条件性地加载 polyfills 和 legacy bundle注入import.meta.env.LEGACY环境变量仅在 legacy 生产构建中为true其余场景现代构建、SSR、开发为false。二、安装与基本用法// vite.config.js import legacy from vitejs/plugin-legacy export default { plugins: [ legacy({ targets: [defaults, not IE 11], }), ], }Terser 说明README 原文要求当使用 Vite 8.1.4 之前的版本或者显式将build.minify设为terser时必须安装 terser 用于压缩npm add -D terser这条要求可以直接在源码中得到印证。src/index.ts#L143-L145 定义了常量legacyOxcMinificationSupportedVersion 8.1.4而 resolveLegacyBuildMinify 的逻辑是function resolveLegacyBuildMinify( minify: BuildOptions[minify], supportsOxc: boolean | undefined, ): BuildOptions[minify] { const usesOxc supportsOxc (minify oxc || minify true) return usesOxc ? oxc : minify ? terser : false }也就是说legacy chunk 的压缩器只有在“Vite ≥ 8.1.4 且minify为oxc或true”时才能走 Oxc其余情况一律回落到terser——这正是 README 里 terser 安装说明的底层原因。若低版本 Vite 下使用minify: oxcconfigResolved 钩子 还会主动打印警告提示升级到 8.1.4 以上。三、选项全解以下按 README 的“Options”章节逐项继承并结合 src/types.ts 与 src/index.ts 的实现补充细节。targets类型string | string[] | { [key: string]: string }默认last 2 versions and not dead, 0.3%, Firefox ESR透传给babel/preset-env作用于legacy chunk的渲染。查询语法兼容 Browserslist 语法。源码中该默认值与取值优先级可以精确看到src/index.ts#L488-L494targets options.targets || browserslistLoadConfig({ path: config.root }) || last 2 versions and not dead, 0.3%, Firefox ESR即显式选项 → 项目内 browserslist 配置源 → 内置默认值与 README “If its not set, plugin-legacy will load the browserslist config sources” 的表述一致。modernTargets类型string | string[]默认edge105, firefox106, chrome105, safari16.4, chromeAndroid105, iOS16.4透传给babel/preset-env用于收集modern chunk的 polyfills此值会覆盖build.target。README 特别提醒除非你同时设置了renderLegacyChunks: false否则不应设置此选项。从源码看该默认值有两处镜像定义src/index.ts#L172-L185一份 Babel/Browserslist 语法的modernTargetsBabel一份 esbuild 语法的modernTargetsEsbuild [es2020, edge105, firefox106, chrome105, safari16.4, ios16.4]。在config钩子中src/index.ts#L301-L308if (options.modernTargets) { const { default: browserslistToEsbuild } await import(browserslist-to-esbuild) config.build.target browserslistToEsbuild(options.modernTargets) } else { config.build.target modernTargetsEsbuild }这正是“覆盖build.target”的实现未显式设置modernTargets时插件会直接把build.target固定为上述现代化浏览器基线。相应的三条黄色警告覆盖build.target、modernTargets覆盖内置目标可能丢失 legacy 与 modern 之间某些浏览器版本、以及 worker 场景误用都在 configResolved 钩子 中发出。polyfills类型boolean | string[]默认true默认情况下基于目标浏览器范围与最终 bundle 中的实际用法由babel/preset-env生态的useBuiltIns: usage检测生成 polyfills chunk设为字符串数组可显式控制包含哪些 polyfill见下文“Polyfill Specifiers”设为false则不生成 polyfill仍会生成做了语法转换的 legacy chunk。实现上用法检测由 src/index.ts#L37-L54 的loadPolyfillPlugins完成[ (await import(babel-plugin-polyfill-corejs3)).default, { method: usage-global, version: _require(core-js/package.json).version, shippedProposals: true }, ], [ (await import(babel-plugin-polyfill-regenerator)).default, { method: usage-global }, ],注意两个细节core-js 版本直接取插件依赖的core-js包版本保证注入路径与实际打包的 core-js 一致shippedProposals: true意味着启用了已“毕业”的提案特性支持。此外因为 SystemJS 依赖PromisegenerateBundle 钩子 里还会额外对探测代码Promise.resolve(); Promise.all();做一次 polyfill 检测必要时自动补上 Promise polyfill。additionalLegacyPolyfills/additionalModernPolyfills类型string[]分别向 legacy / modern 的 polyfills chunk 追加自定义 import。由于基于用法的检测只覆盖ES 语言特性需要手动补 DOM API 之类的 polyfill 时用它们。源码中的处理在 src/index.ts#L249-L269additional*列表中的项被原样加入 polyfill 集合不经过 core-js 路径映射而polyfills数组中的项会做映射——含/的视为 core-js 子路径如es/map→core-js/es/map不含/的视为模块名如es.array.iterator→core-js/modules/es.array.iterator.js以regenerator开头的特殊映射为regenerator-runtime/runtime.js。modernPolyfills类型boolean | string[]默认false开启后会为 modern 构建单独生成一个 polyfills chunk面向“支持 ESM 但不支持某些广泛可用特性”的浏览器。设为字符串数组可显式控制内容。README 特别警告如果未设置modernTargets不建议用true自动检测因为 core-js 3 支持大量前沿特性自动检测会非常激进——即使目标是原生 ESM 浏览器也会注入约 15kb polyfill。替代方案不设modernTargets就避免使用 modern polyfill或者只按需指定具体列表。仓库的 playground/legacy/vite.config-modern-target.js 给出了该选项的典型组合用法legacy({ modernPolyfills: [es.array.at], // 一个不支持 optional catch binding 的浏览器 modernTargets: [chrome 64], renderLegacyChunks: false, })renderLegacyChunks类型boolean默认true设为false禁用 legacy chunk通常配合modernPolyfills使用——即把本插件当作“只给 modern 构建注 polyfill”的工具import legacy from vitejs/plugin-legacy export default { plugins: [ legacy({ modernPolyfills: [/* ... */], renderLegacyChunks: false, }), ], }从源码看renderLegacyChunks: false与renderModernChunks: false不能同时为 false否则插件初始化时直接抛错src/index.ts#L215-L221const genLegacy options.renderLegacyChunks ! false const genModern options.renderModernChunks ! false if (!genLegacy !genModern) { throw new Error(renderLegacyChunks and renderModernChunks cannot be both false) }externalSystemJS类型boolean默认false开启后polyfills-legacy chunk 将不包含systemjs/dist/s.min.js由你自己从外部加载 SystemJS。对应的注入逻辑在 polyfillsPlugin虚拟 polyfill 模块默认拼接import systemjs/dist/s.min.js;excludeSystemJS为真时省略该行。仓库中 playground/legacy/vite.config-no-polyfills-no-systemjs.js 即演示了renderModernChunks: false polyfills: false externalSystemJS: true的极端组合完全去掉 modern chunk 与 polyfill chunk且 polyfill 检测会自动跳过 Promise 探测。renderModernChunks类型boolean默认true设为false时只输出覆盖所有目标浏览器的 legacy bundle。README 还提到一个实用场景本地用file:协议运行项目时typemodule的现代 chunk 可能触发 CORS 限制此时可设renderModernChunks: false只使用 legacy chunk。实现上transformIndexHtml 在!genModern时会把 HTML 里的script typemodule与所有modulepreload链接一并移除if (!genModern) { html html .replace(/script typemodule.*?\/script/g, ) .replace(modulePreloadLinkRE, ) }其中modulePreloadLinkRE的正则行为由专门测试 src/tests/index.spec.ts 覆盖能匹配属性顺序任意、自闭合、多行属性、单引号等写法的modulepreload链接同时不会误伤link relstylesheet、relpreload、xrel等相似标签。assumptions源码中存在README 未列入的选项从 src/types.ts#L32-L37 看插件还接受一个 README 未专门说明的选项/** * see https://babeljs.io/docs/assumptions * * default: {} */ assumptions?: Recordstring, boolean它会随targets一起透传给babel/preset-env与 polyfill 检测流程renderChunk 中的 babelTransformOptions 与 detectPolyfills 均携带assumptions。按 Babel assumptions 的语义可以声明“模块永远只会被加载一次”等假设换取更激进也更小的降级输出——如果你的代码边界满足相应前提可以考虑设置。四、构建时到底发生了什么源码级机制走读README 描述的是“行为”而 src/index.ts 的viteLegacyPlugin返回三个子插件src/index.ts#L899vite:legacy-config、vite:legacy-generate-polyfill-chunk、vite:legacy-post-processenforce: post。下面按构建阶段梳理关键调用链。1.config阶段改写构建配置import.meta.env.LEGACY的注入通过define完成src/index.ts#L311-L318serve 或 SSR 构建时直接是false普通 build 时替换为一个占位标记__VITE_IS_LEGACY__L140留待后面的 chunk 渲染阶段按 chunk 属性分别替换为truelegacy或falsemodern。生成 legacy 时若用户未设build.cssTarget插件会写入chrome61src/index.ts#L283-L290。注释解释得很清楚这是给 CSS 压缩器esbuild的兼容提示真正影响压缩结果的是HexRGBA特性chrome61足以修正该兼容性问题。build.target按前文modernTargets一节所述的规则被覆盖。2.configResolved阶段声明双份 output这是“一份 bundle 两个输出”的关键。createLegacyOutput 基于原始 output 克隆出一份 legacy 输出return { ...options, format: esm, // 先按 ESM 产出renderChunk 中再转 SystemJS entryFileNames: getLegacyOutputFileName(options.entryFileNames), chunkFileNames: getLegacyOutputFileName(options.chunkFileNames), minify: resolveLegacyOutputMinify(config.build.minify, supportsLegacyOxcMinification, es2015), }而 getLegacyOutputFileName 负责把文件名改写为 legacy 形态兼容多种自定义命名用户命名模板生成的 legacy 文件名未设置默认assets/[name]-legacy-[hash].js[name]-[hash].js[name]-legacy-[hash].jscustom[hash].js/custom.[hash:10].js[name]-legacy[hash].js/custom-legacy.[hash:10].jsentry.js无 hashentry-legacy.js随后 configResolved 尾段 把rolldownOptions.output改写为[legacyOutput, ...modernOutputs]即同一份模块图先渲染 legacy 输出再渲染 modern 输出同时注入内部标记isOutputOptionsForLegacyChunks供后续钩子判断当前 output 是否属于 legacy。识别 chunk 身份的规则很直接isLegacyChunk就是看文件名是否包含-legacysrc/index.ts#L1100-L1110。3.renderChunk阶段legacy chunk 的 Babel 管线对每个 legacy chunkrenderChunk管线是两步 Babel 转换第一步babel.transform只跑两个插件产出 AST不产代码babel/plugin-transform-dynamic-importbabel/plugin-transform-modules-systemjs—— 把 ESM 转成 SystemJS 模块。第二步babel.transformFromAstSync应用 presets注意 Babel preset 逆序执行源码注释 L677-L682 明确了有效顺序polyfill 插件babel-plugin-polyfill-corejs3/babel-plugin-polyfill-regenerator均为usage-global先按用法注入 core-js / regenerator 的 importbabel/preset-envbugfixes: true, modules: false, shippedProposals: true做语法降级自定义插件组收尾recordAndRemovePolyfillBabelPlugin把第一步注入的 polyfill import记录后删除polyfill 统一打包到独立的 polyfill chunk避免每个 legacy chunk 重复携带replaceLegacyEnvBabelPlugin / replaceModernEnvBabelPlugin把__VITE_IS_LEGACY__替换为true、__VITE_IS_MODERN__替换为false——import.meta.env.LEGACY的分流在此落地wrapIIFEBabelPlugin把整个模块体包进(function(){...})();IIFE防止 SystemJS 命名冲突。对 modern非 legacychunk同一renderChunk分支还会在modernPolyfills: true时对 chunk 源码执行 detectPolyfills 收集用法级 polyfill并把__VITE_IS_LEGACY__标记改写为false此外对每个入口 modern chunk 用MagicString.prepend注入 legacy 守卫见下节。4.generateBundle阶段polyfill chunk 的“二次构建”polyfill chunk 不是从主 bundle 里切出来的而是另起一次 Vite 构建合成的。buildPolyfillChunk 调用build()其入口是一个虚拟模块\0vite/legacy-polyfills内容由 polyfillsPlugin 生成——即把所有检测/指定的 polyfill 逐行import拼起来再按externalSystemJS决定是否追加import systemjs/dist/s.min.js;load(id) { if (id polyfillId) { return ( [...imports].map((i) import ${JSON.stringify(i)};).join() (excludeSystemJS ? : import systemjs/dist/s.min.js;) ) } }这次子构建还刻意做了限制esbuild: false不做转译optimizeDeps.esbuildOptions.target: es5避免 esbuild 注入 ES2015 语法的 helpersrc/index.ts#L994-L1004。legacy polyfill chunk 以iife格式产出modern polyfill chunk 以es格式产出generateBundle 调用点legacy 侧还会把 Oxc 压缩的compress.target压到es2015“Dont use newer syntax for legacy polyfill chunks”。最后通过ctx.emitFile({ type: prebuilt-chunk, ... })把成品塞回主 bundle并把“每个入口 → polyfill 文件名”记入 map供 HTML 注入阶段查找。5.transformIndexHtml阶段六步注入transformIndexHtml 按固定顺序往产物 HTML 注入脚本源码中用注释标了序号modern polyfills若有 modern polyfill chunk注入script typemodule crossorigin src...importmap若启用了chunkImportMap构建选项注入typesystemjs-importmap的内联脚本headSafari 10nomodule修复注入一段nomodule内联脚本body内容是 snippets.ts#L1-L3 中的safari10NoModuleFixlegacy polyfills注入nomodule脚本idvite-legacy-polyfill、crossorigin、指向 polyfill chunkbodylegacy 入口注入nomodule脚本idvite-legacy-entry入口路径放在data-src属性上L835-L838 注释解释这样做是为了让脚本内容保持恒定从而可以用固定哈希值做 CSP脚本体是 snippets.ts#L7 的System.import(document.getElementById(vite-legacy-entry).getAttribute(data-src))body动态 import 回退注入两段内联 module 脚本head——detectModernBrowserCode与dynamicFallbackInlineCode。第 6 步就是“支持 ESM 但缺广泛特性”浏览器的运行时兜底其机制见下一节。五、支持 ESM 但缺少广泛特性的浏览器Legacy Edge 场景README 指出插件让现代构建可以原样使用“广泛可用”的特性同时让那些支持原生 ESM 却不支持这些特性的浏览器回落到 legacy 构建——方式是在 modern 构建中注入运行时检查需要时改用 SystemJS 运行时加载 legacy bundle。代价有二现代 bundle 会在所有 ESM 浏览器中下载现代 bundle 在缺少这些特性的浏览器中会抛错。被认定为“广泛可用”的特性是动态 import、async generator、import.meta.resolve。这套运行时检查的完整代码在 src/snippets.ts// detect support via syntax errors export const detectModernBrowserDetector: string import.meta.url;import(_).catch(()1);(async function*(){})().next() export const detectModernBrowserCode: string import${createDetectImportMetaResolveSupportModule(null)};${detectModernBrowserDetector};window.__vite_is_modern_browsertrue检测代码通过触发语法错误/运行时行为来判断支持度import.meta.url需要import.meta、import(_)需要动态 import、async generator 表达式import.meta.resolve的检查则放在一段data:text/javascript内联模块里不支持时直接throwsnippets.ts#L11-L22 的注释还说明由于 Safari 15.x 及以下的 bug每个内联模块必须内容唯一否则错误只在首次导入时抛出检测通过则设置window.__vite_is_modern_browser truedynamicFallbackInlineCode 在非现代浏览器中从vite-legacy-polyfill脚本取src动态插入 polyfill 脚本并加载 legacy 入口同时console.warn提示“上面的语法错误与下面的相同错误应被忽略”modern 入口 chunk 头部还会被前置 createModernChunkLegacyGuard 生成的守卫一段同样依赖语法错误检测的import与__vite_legacy_guard导出确保现代 bundle 真在旧浏览器执行时尽早失败把控制权交给上面的回退脚本。六、Polyfill Specifierspolyfills与modernPolyfills接受的字符串可以是以下两类任意core-js 3 子路径 import——例如es/map会 importcore-js/es/map任意core-js 3 单模块——例如es.array.iterator会 importcore-js/modules/es.array.iterator.js。示例README 原文import legacy from vitejs/plugin-legacy export default { plugins: [ legacy({ polyfills: [es.promise.finally, es/map, es/set], modernPolyfills: [es.promise.finally], }), ], }这与 src/index.ts#L242-L269 中“含/走子路径、不含/走core-js/modules/*.js”的映射逻辑一一对应。七、内容安全策略CSP配合README 明确插件需要内联脚本用于 Safari 10.1nomodule修复、SystemJS 初始化和动态 import 回退。若你有严格的 CSP 策略需要把对应哈希加入script-src。哈希值不含sha256-前缀可通过以下方式获取import { cspHashes } from vitejs/plugin-legacy当前值sha256-MS6/3FCg4WjP9gwgaBGwLpRCY6fZBgwmhVCdrPrNf3Esha256-tQjf8gvb2ROOMapIxFvFAYBeUJ0v1HCbOcSmDNXGtDosha256-w36slEqa9euNKxfvkwLLGsDIr3rsZXpZxtmRh8Awsha256-5XkZFazzJo8n0iOP4ti/cLCMUudTf//Mzkb7xNPXIc这几个哈希并非手工维护的魔法值src/index.ts#L1170-L1175 中cspHashes是对四段注入代码safari10NoModuleFix、systemJSInlineCode、detectModernBrowserCode、dynamicFallbackInlineCode实时做crypto.hash(sha256, ..., base64)得到的并且 src/tests/readme.spec.ts 有一条 CI 测试用正则从 README 抽取所有sha256-...值并与运行时计算的cspHashes严格比对——文档漂移会直接挂测试。README 的两条运维建议同样值得保留这些值可能随 minor 版本变化建议从导出的cspHashes动态生成 CSP 头若手工复制请用~锁定 minor 版本使用regenerator-runtimepolyfill 时若目标浏览器没有globalThis含 IE 11它会退化为动态Function(...)调用从而违反 CSP可通过在additionalLegacyPolyfills中加入core-js/proposals/global-this来定义globalThis规避。八、使用限制与注意事项从 README 与源码可以确认的约束仅作用于生产构建apply: build两个构建期子插件且所有关键钩子在config.build.ssr为真时直接return——SSR 构建不做 legacy 处理不支持库模式configResolved 钩子 中if (_config.build.lib) throw new Error(vitejs/plugin-legacy does not support library mode.)不要传给worker.pluginsconfigResolved中检测到config.isWorker会警告“生成 worker 的 legacy chunk 不受支持”src/index.ts#L342-L348压缩依赖 terser 的版本条件见第二节Vite 8.1.4 或显式minify: terser时必须npm add -D terser覆盖build.target的警告插件会用自己的 modern 基线覆盖build.target如果你确实需要自定义 modern 侧的构建目标正确做法是通过插件选项modernTargets传入而不是改build.target后者会触发黄色警告file:协议本地验证renderModernChunks: false可规避typemodule在本地文件协议下的 CORS 问题。九、仓库内的端到端验证配置插件的行为不是“文档说了算”——playground/legacy 提供了覆盖各选项组合的真实构建场景可作为你落地时的参照配置vite.config.jstargets: IE 11modernPolyfills: true多入口index.htmlnested/index.html、base: ./、自定义chunkFileNames验证 legacy 命名改写对自定义模板的适配并在__test__()里剥离typemodule脚本与nomodule属性让 e2e 测试实际跑 legacy 产物vite.config-modern-target.jsrenderLegacyChunks: falsemodernPolyfills: [es.array.at]modernTargets: [chrome 64]验证“只给 modern 构建注 polyfill”模式vite.config-no-polyfills-no-systemjs.jsrenderModernChunks: falsepolyfills: falseexternalSystemJS: true验证纯 legacy、无 polyfill chunk、外部 SystemJS 的组合目录下还有vite.config-chunk-importmap.js、vite.config-custom-filename.js、vite.config-multiple-output.js、vite.config-watch.js等分别对应 importmap 注入、自定义文件名、多 output 与 watch 模式场景对应测试位于 playground/legacy/tests。十、参考README 末尾给出的延伸阅读主题对应社区与博客文章此处不复述外部链接Vue CLI 的 modern mode、《Using Native JavaScript Modules in Production Today》、rollup-native-modules-boilerplate。它们与本插件的“modern nomodule legacy 双份构建”策略一脉相承可作为理解该方案历史背景的入口。要点回顾vitejs/plugin-legacy 双份 outputmodern ESM legacy SystemJS 命名 chunk 用法级 core-js/regenerator 按需 polyfill chunk nomodule条件加载 import.meta.env.LEGACY分流 面向缺广泛特性浏览器的运行时语法探测回退 可导出cspHashes的 CSP 支持。配置时以targets/modernTargets控制两端边界以polyfills/modernPolyfills/renderLegacyChunks/renderModernChunks/externalSystemJS控制产物形态即可覆盖从 IE 11 到 Legacy Edge 的全部兼容场景。【免费下载链接】viteNext generation frontend tooling. Its fast!项目地址: https://gitcode.com/GitHub_Trending/vi/vite创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →