首页
/ Vite @vitejs/plugin-legacy 版本演进全景:从 CHANGELOG 看 Legacy 构建插件的技术发展史

Vite @vitejs/plugin-legacy 版本演进全景:从 CHANGELOG 看 Legacy 构建插件的技术发展史

2026-09-04 16:17:32作者:牧宁李

@vitejs/plugin-legacy 是 Vite 官方提供的旧浏览器兼容构建插件,负责在生产构建时生成 SystemJS 格式的 legacy chunk、按需注入 polyfill 并改造 HTML 以兼容不支持原生 ESM 的浏览器。本文以仓库中的 CHANGELOG.md 为主体,梳理该插件从 1.0.0(2021-01-07)到 8.2.3(2026-08-06)的完整版本脉络:每一代 major 版本的破坏性变更、配置选项的逐步丰富、以及近期与 Rolldown、Oxc 生态融合的关键改动,并结合 插件源码playground 测试配置 说明这些变更记录在实现中的具体落点。

一、插件定位与 CHANGELOG 覆盖范围

根据 README.md,Vite 的最低浏览器支持目标是原生 ESM 动态 import()import.meta,而该插件的作用正是"为生产构建中不支持这些特性的旧浏览器提供支持"。默认行为包含四件事:

  • 为最终产物中的每个 chunk 生成对应的 legacy chunk,用 @babel/preset-env 转换并输出为 SystemJS 模块(代码分割仍然受支持);
  • 生成一个 polyfill chunk,包含 SystemJS 运行时,以及根据指定浏览器目标和代码实际用量检测出的必要 polyfill;
  • 向生成的 HTML 注入 <script nomodule> 标签,让缺乏这些特性的浏览器条件性地加载 polyfill 和 legacy bundle;
  • 注入 import.meta.env.LEGACY 环境变量,仅在 legacy 生产构建中为 true,其余场景为 false

CHANGELOG 覆盖的版本跨度约为 5 年 7 个月:起点是 2021 年 1 月 7 日发布的 1.0.0,终点是 2026 年 8 月 6 日发布的 8.2.3。当前 package.json 中声明的版本号为 8.2.3,与 CHANGELOG 最后一节完全对应。

二、八个 Major 版本:破坏性变更主线

CHANGELOG 中带有 ⚠ BREAKING CHANGES 标记的条目是整个插件演进中最值得关注的部分。按时间顺序整理如下。

1.x 时代(2021 年初):奠定插件骨架

  • 1.0.0(2021-01-07):插件首次发布(提交 8c34870)。同版本还包含两个关键修复——当 bundle 未使用 Promise 时自动补充 Promise polyfill,以及避免对 legacy chunk 执行 esbuild 转换;性能方面改用 @babel/standalone 并引入懒加载。
  • 1.1.0(2021-01-07):引入"常量内联脚本 + 提供 CSP 哈希"机制(提交 72107cd),这是后来 cspHashes 导出的前身。
  • 1.2.0(2021-01-11):新增 additionalLegacyPolyfills 选项(PR #1475)。
  • 1.2.2(2021-01-25):明确"esbuild minify 与 legacy 插件不兼容",直接使用 esbuild 压缩会抛出错误;同版本改用 compact 输出转换 legacy chunk。
  • 1.3.0(2021-02-11):注入 import.meta.env.LEGACY(提交 416f190),补齐了 README 中描述的第四个默认行为。
  • 1.4.0(2021-05-17):关闭 babel loose 模式、恢复 dynamic-import-polyfill,并补充了 IE11 相关的文档说明。
  • 1.5.0(2021-07-27):legacy 场景下的动态 import 回退方案(PR #3885)。
  • 1.6.0(2021-09-29):新增 externalSystemJS 选项(PR #5024),允许在 polyfills-legacy chunk 中排除 systemjs/dist/s.min.js
  • 1.6.1(2021-10-05):将 terser 设为默认压缩器(PR #5168)。

2.0:相对 base 与 terser 可选化

  • 2.0.0-alpha.1(2022-05-19):两条破坏性变更——"提升 targets"(PR #8045)与"relative base"(PR #7644),后者使插件必须适配相对 base 下的资源路径。
  • 2.0.0-alpha.2(2022-06-19):terser 变为可选依赖(PR #8049)。这是插件历史上重要的依赖策略调整:此后使用 terser 压缩时才需要自行安装(当前 README 中关于"Vite 8.1.4 以下版本或使用 terser 压缩时必须安装 terser"的说明即源于此)。同版本修复了空 base 导致 import 失败的问题(PR #4212)、让 polyfill chunk 尊重 entryFileNames 配置(PR #8247),并改进默认 polyfill 注入策略(PR #8312)。
  • 2.0.0(2022-07-13):正式版本。
  • 2.1.0-beta.0(2022-08-29):两个重构值得注意——"构建 polyfill chunk"(PR #9639)把 polyfill 的构建从内联拼接改为独立 chunk 构建流程;"移除 Vite 2 时代代码"(PR #9640)。
  • 2.2.0(2022-09-19):强制设置 build.target(PR #10072),保证现代 chunk 以插件定义的现代浏览器目标构建,而不是沿用 Vite 的默认 target。
  • 2.3.0-beta.0(2022-10-05):修复了强制 build.target 的副作用——当 renderLegacyChunks=false 时不再强制覆盖(PR #10220,修复 issue #10201),这一行为在 5.3.2 中再次修正为"禁用 legacy chunk 时仍尊重 modernTargets 选项"(PR #15789)。
  • 2.3.0 / 3.0.0(2022-12-09):随 Rollup 3 迁移(PR #9870),并对齐默认 chunk 与资源文件名(PR #10927)。

4.0:引入 Browserslist 与提升 modern target

  • 4.0.0(2023-02-02):两条破坏性变更:
    • modern target 提升到支持 async generator(PR #11896):modern 构建开始使用 async function* 作为运行时代码检测能力的一部分;
    • 支持 Browserslist 并更新默认 target(PR #11318):targets 选项从此完全兼容 Browserslist 查询语法,未显式设置时会读取项目中的 browserslist 配置源,再回退到默认值 last 2 versions and not dead, > 0.3%, Firefox ESR。 同版本还修复了 legacy sourcemap 不生成的问题(PR #11841),并优化了 cspHashes 数组的实现(PR #11734)。
  • 4.0.2(2023-03-16):SSR 构建不再覆盖 build.target(PR #12171)。
  • 4.0.3(2023-04-25):支持 file: 协议(PR #8524),为"仅输出 legacy 构建"的本地静态部署铺路。
  • 4.1.0(2023-07-06):新增"仅输出 legacy 构建"选项(PR #10139),即后来的 renderModernChunks: false 能力。

5.0:命名规范固化与清理

  • 5.0.0(2023-11-16):四条破坏性变更:
    • x.[hash].js 重命名为 x-legacy.[hash].js(PR #11599):legacy 文件名从此显式带 -legacy 标记,避免与 modern 产物混淆;
    • 移除 ignoreBrowserslistConfig 选项(PR #14429);
    • 最低 Node 版本提升到 18(PR #14030);
    • 提升 Vite peer 依赖(PR #15004)。 功能方面导出了 Options 类型(PR #14933)——对应现在 types.ts 中被导出的 Options 接口。此版本还有几个关键修复:modern polyfill 自动检测注入过多/过少 polyfill(PR #14428 / PR #16367 后续版本)、为 modern polyfill chunk 添加守卫(PR #13719)、避免 terser 把现代浏览器检测代码摇掉而加入显式 invoke(PR #14968)。

6.0 与 7.0:依赖与运行环境收紧

  • 6.0.0(2024-11-26):唯一破坏性变更是"在版本范围中移除 Node 21 支持"(PR #18729);同时把 terser peer 依赖提升到 ^5.16(PR #18772)。
  • 6.1.0(2025-04-16):两个重要功能——assumptions 选项(PR #19719,透传 Babel 的 assumptions 配置)与 sourcemapBaseUrl 支持(PR #19281)。
  • 7.0.0(2025-06-24):三条破坏性变更:
    • 移除现代 Android WebView 的 location.protocol != "file:" 条件(PR #20179);
    • 要求 Node 版本提升到 20.19+ / 22.12+,并移除 CJS 构建(PR #20032),插件从此纯 ESM 分发——当前 package.json"type": "module"engines.node: "^20.19.0 || >=22.12.0" 即这一条的落地结果;
    • 移除 Node 18 支持(PR #19972)。
  • 7.x 系列还有若干值得记录的修复:modernTargets 应当设置 build.target(7.2.0,PR #20393);legacy chunk 未生成时不再降级 CSS(7.0.1,PR #20392);检测 polyfills 时跳过 lowering 的性能优化(7.0.1,PR #20387);polyfill 导入只 import 一次的修复(6.0.1,PR #19152)。

8.0:modern 阈值提升到 import.meta.resolve

  • 8.0.0(2026-03-12):两条核心变更:
    • modern 浏览器阈值提升到支持 import.meta.resolve(PR #21662):modern chunk 的"广泛可用特性"检测从"动态 import + async generator"扩展为"动态 import + async generator + import.meta.resolve"。这一点可以在 snippets.ts 中直接验证:detectModernBrowserDetector 之外,现在新增了 createDetectImportMetaResolveSupportModule,通过内联 data: URL 模块执行 if(!import.meta.resolve)throw Error(...) 来在入口 chunk 加载阶段提前抛出错误,阻止 modern chunk 在不支持的浏览器中继续执行;
    • rolldown-vite 的大合并(PR #21189):Vite 核心迁移到 Rolldown 后,插件随之完成适配。 同版本的其他修复包括:legacy chunk 跳过 preload helper(PR #21607)、polyfill chunk 改用 prebuilt-chunk 方式(PR #21498)、升级 peer 依赖 Vite 到 8。
  • 8.0.0 的四个 beta 版(8.0.0-beta.0 ~ beta.3,2025-12-03 ~ 2026-02-12)记录了从 7.2.1 到 Rolldown 合并之间的渐进过程。

三、8.0 之后的近期演进(8.0.1 ~ 8.2.3)

CHANGELOG 最新的十余节条目(2026-03 ~ 2026-08)反映了插件与新一代工具链磨合期的密集修复:

版本 日期 关键变更
8.0.1 2026-03-26 Safari 15 错误缓存 bug 的 workaround(PR #22028)——即 snippets.ts 注释中提到的"由于 Safari 15.x 及以下的 bug,每个内联模块必须唯一"
8.1.0-beta.0 2026-06-15 构建 chunk importmap 支持(PR #21580);rolldownOptions 尽可能替代旧 API(PR #21205)
8.2.0 2026-07-09 优先使用 Oxc 作为压缩器(PR #22468,修复 issue #21973);统一使用 babel-plugin-polyfill-* 系列(PR #22874)
8.2.1 2026-07-16 压缩 polyfill chunk 时不再使用新语法(PR #22939)
8.2.2 2026-07-23 压缩 legacy chunk 时不再使用新语法(PR #23013);bundled-dev 使用客户端 HMR(PR #22961);magic-string 升级到 v1
8.2.3 2026-08-06 sharedPlugins: true 场景下 client chunkImportMap 正常工作(PR #23184)

四、从源码验证:变更记录的落地形态

4.1 配置项全集与默认值

CHANGELOG 中逐版本引入的选项,在 types.ts 中形成了当前的 Options 接口,与 README 的 Options 章节一一对应:

选项 类型 默认值 引入版本(CHANGELOG)
targets string | string[] | Record<string, string> 'last 2 versions and not dead, > 0.3%, Firefox ESR' 4.0.0(Browserslist 化)
modernTargets string | string[] 'edge>=105, firefox>=106, chrome>=105, safari>=16.4, chromeAndroid>=105, iOS>=16.4' 5.3.0(PR #15506)
polyfills boolean | string[] true 早期
additionalLegacyPolyfills string[] 1.2.0(PR #1475)
additionalModernPolyfills string[] 5.4.0(PR #16514)
modernPolyfills boolean | string[] false 早期
renderLegacyChunks boolean true 早期
externalSystemJS boolean false 1.6.0(PR #5024)
renderModernChunks boolean true 4.1.0(PR #10139)
assumptions Record<string, boolean> {} 6.1.0(PR #19719)

src/index.ts 中可以看到 targets 的三级解析链与 README 描述完全一致:

targets =
  options.targets ||
  browserslistLoadConfig({ path: config.root }) ||
  'last 2 versions and not dead, > 0.3%, Firefox ESR'

modernTargets 的默认值常量 modernTargetsBabel(src/index.ts#L184-L185)正是 8.0.0 提升阈值后的取值,注释中明确写着"支持动态 import + import.meta.resolve + async generator 的浏览器"。注意 types.tsmodernTargets 的 JSDoc 默认值(edge>=79...)是旧版注释,实际代码中的默认值以 modernTargetsBabel 常量为准。

4.2 legacy 文件命名规则(5.0.0 破坏性变更的落地)

5.0.0 的 x.[hash].js → x-legacy.[hash].js 重命名规则,完整实现在 getLegacyOutputFileNamesrc/index.ts#L496-L528)中:

  • 自定义文件名含 [name] 占位符:[name]-[hash].js → [name]-legacy-[hash].js
  • 文件名含非首位的 [hash](如 custom[hash].jscustom-[hash].jscustom.[hash:10].js):在 hash 前插入 -legacy
  • 普通文件名:entry.js → entry-legacy.jsentry.min.js → entry-legacy.min.js
  • 未配置时默认使用 assets/[name]-legacy-[hash].js

这一逻辑与 CHANGELOG 中 5.3.0 的"文件名优化"(PR #15115)和"支持文件名中 hash 前任意分隔符"(PR #15170)两个条目直接对应。playground/legacy 中的 vite.config.js 专门用 chunk-X.[hash].jschunk-X-[hash].jschunk-X[hash].js 三种命名(对应 custom0.js / custom1.js / custom2.js)覆盖了这些分支。

4.3 modern/legacy 双 chunk 的输出编排

插件通过 configResolved 钩子(src/index.ts#L547-L559)把用户配置的 rolldownOptions.output 复制为两份:先插入一份 format: 'esm' 且文件名为 -legacy 版本的 legacy 输出,再追加 modern 输出(当 renderModernChunksfalse 时只有 legacy 一份)。这与 CHANGELOG 中 4.1.0"仅输出 legacy 构建"、2.3.0 起围绕 renderLegacyChunks/build.target 覆盖的多次修正共同构成了双输出的完整行为。此外插件会向配置注入 isOutputOptionsForLegacyChunks 内部标记,供后续钩子区分当前渲染的是哪一份产物。

4.4 压缩器策略:terser → oxc 的迁移

CHANGELOG 中 8.2.0"优先使用 oxc 作为压缩器"在源码中体现为版本协商逻辑:src/index.ts#L143-L145 定义 legacyOxcMinificationSupportedVersion = '8.1.4',注释说明"legacy 的 Oxc 压缩需要 plugin-legacy 与 Vite 核心协同支持"。resolveLegacyBuildMinifyresolveLegacyOutputMinify 两个函数(src/index.ts#L190-L207)实现策略:当前 Vite 版本支持 Oxc 协同时,legacy 产物沿用 oxc 压缩(并强制 target: 'es2015',呼应 8.2.1/8.2.2"压缩时不用新语法"的两条修复);否则回退为 terserconfigResolved 中还会校验 Vite 版本并给出升级提示(src/index.ts#L471-L478)。这正是 README 中"Terser 必须在 Vite 8.1.4 以下版本或显式指定 build.minify: 'terser' 时安装"这句话的由来,也与 package.jsonpeerDependencies: { terser: ^5.16.0, vite: ^8.0.0 } 的声明一致。

4.5 polyfill 检测:usage-global 与确定性分组

  • 8.0.0 的重构"统一使用 babel-plugin-polyfill-*"(PR #22874)体现在 loadPolyfillPluginssrc/index.ts#L37-L54):babel-plugin-polyfill-corejs3babel-plugin-polyfill-regenerator 均以 method: 'usage-global' 方式注入,core-js 版本直接从 core-js/package.json 读取;
  • 5.4.1 的"按输出分组发现的 polyfill,改进确定性"(PR #17347 / PR #16566)对应 outputToChunkFileNameToPolyfills 这个 WeakMap(src/index.ts#L237-L240):注释明确指出"renderChunk 钩子中 polyfill 发现可能不确定,因此按输出对排序后的 chunk 文件名分组";
  • 7.0.1 的"检测 polyfills 时跳过 lowering"(PR #20387)与 5.4.0"modern polyfill 自动检测注入不足"(PR #16367)等修复,共同保证了 usage 检测结果的稳定与准确。
  • SystemJS 依赖 Promise 的特性在 generateBundle 中通过检测 Promise.resolve(); Promise.all(); 是否需要 polyfill 来保证(src/index.ts#L410-L419)。

4.6 import.meta.env.LEGACY 的注入方式

CHANGELOG 1.3.0 引入的 import.meta.env.LEGACY 在 8.x 源码中采用"标记 + 双输出覆写"方案:config 钩子中 defineimport.meta.env.LEGACY 映射为 __VITE_IS_LEGACY__ 标记(SSR 与 serve 场景恒为 false,src/index.ts#L311-L318);modern 输出的 renderChunk 中把标记覆写为 false(src/index.ts#L613-L623),legacy 输出则覆写为 true。1.3.1 中"防止 import.meta.env.LEGACY 被常量折叠"的修复,保证了这套方案在压缩后仍然可区分。

五、使用方式与配套能力

5.1 基本用法

README 给出的标准接入方式(对应当前 8.x 版本):

// vite.config.js
import legacy from '@vitejs/plugin-legacy'

export default {
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11'],
    }),
  ],
}

需要 terser 时(Vite 8.1.4 以下或显式 build.minify: 'terser')执行 npm add -D terser

仓库自带的 playground/legacy/vite.config.js 展示了更贴近真实场景的完整配置:多入口(index.html + nested/index.html)、base: './'targets: 'IE 11'modernPolyfills: true,并用 chunkFileNames 函数演示了 5.x 版本引入的自定义文件名分支。其 __test__() 钩子还会在构建后移除 <script type="module"> 并把 <script nomodule> 改写为普通 script,强制测试运行 legacy bundle——对应的断言在 legacy.spec.ts 及其 ssr/no-polyfills/modern-target/chunk-importmap/ 等子目录的测试中,覆盖了 SSR 构建、禁用 polyfill、modern target、chunk importmap 等 CHANGELOG 各版本引入的能力。

5.2 仅现代构建的 polyfill 注入

README 强调 modernTargets 只应在 renderLegacyChunks: false 时设置,典型用法(对应 5.3.0 引入的选项与 4.1.0 引入的能力组合):

legacy({
  modernPolyfills: [/* ... */],
  renderLegacyChunks: false,
})

src/index.tsrenderLegacyChunksrenderModernChunks 同时为 false 会直接抛错(src/index.ts#L217-L221),源码中还有 renderLegacyChunks=false 时不强制设置 build.target 的分支(对应 2.3.0-beta.0 的 PR #10220)。

5.3 polyfill 说明符规则

polyfills / modernPolyfills 数组中的字符串支持两种形式(README 的 Polyfill Specifiers 章节):

  • core-js 3 的子导入路径,如 es/map → 导入 core-js/es/map
  • core-js 3 的单模块,如 es.array.iterator → 导入 core-js/modules/es.array.iterator.js

源码中的归一化逻辑(src/index.ts#L254-L263)与之一致:含 / 视为子路径 core-js/${i},不含则视为模块 core-js/modules/${i}.js;以 regenerator 开头的条目映射到 regenerator-runtime/runtime.js。示例配置:

legacy({
  polyfills: ['es.promise.finally', 'es/map', 'es/set'],
  modernPolyfills: ['es.promise.finally'],
})

5.4 CSP 支持

插件内联了 Safari 10.1 nomodule 修复、SystemJS 初始化和动态 import 回退等脚本,严格 CSP 环境需要把对应哈希加入 script-src。哈希值(不含 sha256- 前缀)可通过导入 cspHashes 获取:

import { cspHashes } from '@vitejs/plugin-legacy'

README 特别提示:这些值可能在次版本之间变化(5.1.0 的文档修正亦强调了这点),因此推荐从导出的 cspHashes 变量生成 CSP 头,而不是手工抄写固定值;若必须手工维护,应使用 ~ 锁定次版本。当前值列表见 README 的 CSP 章节。另外当使用 regenerator-runtime 且目标浏览器没有 globalThis(如 IE 11)时,它可能触发动态 Function(...) 调用而违反 CSP,可把 core-js/proposals/global-this 加入 additionalLegacyPolyfills 解决。

5.5 注意事项与限制

  • 仅作用于 build 阶段(1.8.1 的文档说明):插件只在生产构建生效,dev 模式不做 legacy 处理(源码中 apply: 'build' 与 babel 懒加载印证了这一点);
  • 不支持 library modeconfigResolved 中遇到 build.lib 直接抛错(src/index.ts#L460-L463);
  • SSR 构建被跳过config 钩子仅在 !config.build?.ssr 时改写构建配置(src/index.ts#L278),renderChunk/generateBundle 对 SSR 直接返回;
  • 不要传入 worker.pluginsconfigResolved 会对 config.isWorker 发出警告(src/index.ts#L342-L348,对应 6.0.2 的 PR #19079),legacy 插件不支持为 worker 生成 legacy chunk;
  • modernPolyfills: true 慎用:core-js@3 因覆盖大量新特性而注入激进,即使只面向原生 ESM 浏览器也会注入约 15kb 的 polyfill(README 建议配合 modernTargets 使用)。

六、环境要求与验证途径

综合 CHANGELOG 的破坏性变更条目与 package.json,当前 8.x 版本的前提条件为:

  • Node.js:^20.19.0 || >=22.12.0(7.0.0 提升、8.x 延续);
  • Vite:^8.0.0(8.0.0 时提升);
  • terser:仅在使用 terser 压缩路径时需要,^5.16.0(6.0.0 时提升)。

验证插件行为的最佳途径是运行仓库自带的 playground:playground/legacy 下的 __tests__/ 目录包含 legacy 构建主流程、SSR(ssr/client-and-ssr/)、禁用 polyfill(no-polyfills/no-polyfills-no-systemjs/)、modern target(modern-target/)、chunk importmap(chunk-importmap/)、watch 模式样式入口(watch/)等多组端到端测试,每个测试都直接断言 dist 产物中的 legacy/modern 文件名、nomodule script 与 polyfill chunk 内容,是核对本文所述各版本行为最可靠的依据。

七、小结

从 CHANGELOG 可以清晰读出 @vitejs/plugin-legacy 的三条演进主线:

  1. 能力主线:从 1.x 的双 bundle + nomodule 基础架构,到 4.0.0 全面 Browserslist 化、5.x 的 modernTargets/additionalModernPolyfills 精细化控制、6.1.0 的 assumptions 透传,插件把"legacy 兼容"逐步扩展为"legacy 与 modern 两条构建管线的完整控制面";
  2. 约定主线x-legacy.[hash].js 文件命名(5.0.0)、import.meta.resolve 检测阈值(8.0.0)、terser 可选化(2.0.0-alpha.2)等破坏性变更,每一次都在源码中以明确的常量与分支落地;
  3. 工具链主线:跟随 Vite 核心的 Rollup 3(3.0.0)、Rolldown 合并(8.0.0)迁移,并在 8.2.0 起把压缩路径从 terser 优先转向 Oxc 优先、terser 回退,同时持续修复压缩新语法泄漏(8.2.1/8.2.2)与 Safari 兼容性问题(8.0.1、snippets 中唯一化内联模块)。

对维护者而言,这份 CHANGELOG 加上 src/index.tssrc/snippets.tsplayground/legacy 测试矩阵,构成了一套完整的"变更记录—源码落点—行为验证"对照体系。

登录后查看全文
热门项目推荐
相关项目推荐