先把结论摊开说:plugin-legacy
是给“还吃不动现代 ESM 和新 API”的老浏览器准备的“加餐”。真有用户在用老设备,我们就“花点小钱买安心”;要是你站点 99% 都是新版 Chrome/Safari,劝你忍痛割爱,别为了那 1% 把所有人拖慢。
它到底干啥?#
一句话:打两套包,老少咸宜。
- 现代包(ESM):给现代浏览器吃的,轻巧、快。
- 祖传包(Legacy):给不支持 ESM 的老家伙吃的,用 SystemJS 加一堆 polyfill,能跑但会重一些。
- 加载策略:现代浏览器走
<script type="module">
,老浏览器走<script nomodule>
;两套互不打扰,谁该吃谁自己认路。
[!tip] 这波属于“多端投喂”:不强迫所有人都吃软饭,但也不饿着老设备。
什么时候该上?三问做决策(别拍脑袋)#
- 你的用户里有 iOS 12/13、Android 7/8 的长尾吗?
- 有嵌套在古早 WebView(例如某些 X5/厂商定制)的业务必须跑吗?
- 上线后“白屏/报错”的反馈来自老设备吗?(埋点+日志说话)
- 三问“有两个 yes”,就开;全都“no”,老老实实省下来,别压榨构建时间。
快速上手:三步走就能跑#
安装
npm i -D @vitejs/plugin-legacy# 或 pnpm add -D @vitejs/plugin-legacy
最小配置
import { defineConfig } from 'vite'import legacy from '@vitejs/plugin-legacy'
export default defineConfig({ plugins: [ legacy({ targets: ['defaults', 'not IE 11', 'iOS >= 12', 'Android >= 7'], modernPolyfills: true, // 给“准现代”浏览器补上少数新 API additionalLegacyPolyfills: ['regenerator-runtime/runtime'], // async/await 兜底 renderLegacyChunks: true, // 产出独立的 -legacy 包 externalSystemJS: false, // 内联/本地提供 SystemJS(默认 false) }), ],})
怎么验?
- 构建后你会看到
polyfills-legacy
、index-legacy
之类产物。 - 用旧设备/旧 WebView 打开页面,Network 里能看到
-legacy
资源在跑;现代浏览器不会去加载它们。
配置项#
- targets
- 写法基本等于 Browserslist,决定“老的不老”的分界线。
- 建议跟你家
.browserslistrc
一致,别前后打架。
- modernPolyfills
- true 时,给“差一点就现代”的浏览器(比如 Safari 13)补几个关键 API,避免“一口气噎着”。
- additionalLegacyPolyfills
- 点名加一些你用到的 API 垫片,比如
regenerator-runtime/runtime
(async/await)、es.array.flat
等。
- 点名加一些你用到的 API 垫片,比如
- renderLegacyChunks
- 设为 true(默认)会生成独立的
-legacy
产物,老浏览器才会下载;这也是我们要的“差异装载”。
- 设为 true(默认)会生成独立的
- externalSystemJS
- true 表示不内联 SystemJS,需要你自己提供(CDN 或自托管)。一般保持 false 省心。
[!warning] 名字可能不同版本有小差异,但核心思路不变:一套目标(targets)+ 若干补丁(polyfills)+ 两套产物(modern/legacy)。
和 Browserslist / Autoprefixer / Babel 的相处之道#
- 思路统一:把“目标浏览器矩阵”写在
.browserslistrc
里,一处约定,多处受益。 - CSS 走 Autoprefixer,JS 走
plugin-legacy
;如果你已经用了 Babel 的preset-env
+core-js
,注意别“双重垫片”,否则白白长肉。 - Vite 项目一般不主动上 Babel 全家桶,除非你确实有特殊语法链路需求。
示例 .browserslistrc
> 0.5%last 2 versionsnot deadiOS >= 12Android >= 7
它是怎么工作的?(点到为止,够用了)#
- 构建期:Vite 先出一套“现代 bundle”,然后
plugin-legacy
再把代码过一遍“降级机”,生成-legacy
的 SystemJS 版本,并拆出一个polyfills-legacy
。 - 运行时:HTML 里会同时注入
<script type="module">
(现代)和<script nomodule>
(老旧);浏览器自己挑。 - Safari 的“noModule Bug”之类历史遗留,插件会顺带打上胶水脚本,不用你操心。
真实场景怎么配?(两档模板)#
- 标准档(大多数团队够用了)
legacy({ targets: ['defaults', 'not IE 11', 'iOS >= 12', 'Android >= 7'], modernPolyfills: true, additionalLegacyPolyfills: ['regenerator-runtime/runtime'],})
- 进阶稳妥档(更保守,体积和构建时间略涨)
legacy({ targets: ['>0.3%', 'last 3 versions', 'iOS >= 12', 'Android >= 6'], modernPolyfills: true, additionalLegacyPolyfills: [ 'regenerator-runtime/runtime', 'core-js/features/array/flat', 'core-js/features/promise/finally', ], renderLegacyChunks: true,})
成本账本:别糊里糊涂就开了#
- 体积:多一套
-legacy
,再加 polyfills,包会“长点肉”。幸运的是,现代浏览器根本不吃这套,不会拖慢主流用户。 - 构建时间:会慢一些,程度视项目而定。CI 时间要预留。
- 维护:偶尔会遇到“某 API 到底谁来垫片”的扯皮,要统一到
.browserslistrc
,别各说各话。
一句话:只要你的老设备用户是真实的、可观的,这点成本值。
踩坑锦集(都是血泪)#
- 双重 Polyfill
- 你既用
plugin-legacy
又在入口手动import 'core-js/stable'
?两份一起上,白白重。
- 你既用
- 第三方 Polyfill CDN
- 线上拉别人家的 polyfill,时好时坏;建议自托管或随构建产出,别把命交给外人。
- sticky/100vh 等 CSS 坑
- 别把 JS 兼容性工具当万能钥匙,CSS 兼容还得靠 Autoprefixer +
@supports
降级方案。
- 别把 JS 兼容性工具当万能钥匙,CSS 兼容还得靠 Autoprefixer +
- X5/古早 WebView
- 有时不仅是“是否支持 ESM”,还牵扯安全策略、内置拦截。准备一个“纯静态降级版”兜底,别硬杠。
排查套路:怎么确认 legacy 真生效了?#
- Network 面板:旧设备加载了
polyfills-legacy
和index-legacy
?OK。 - Elements/Source:能看到注入的
nomodule
脚本?OK。 - 控制变量:把
targets
暂时改保守一点,看旧设备是否“起死回生”,能迅速验证思路。
和团队怎么对齐?(一页纸共识)#
- 把用户浏览器分布拉出来(Analytics/埋点),定一个“可解释”的最低版本线。
.browserslistrc
一处生效:Autoprefixer、plugin-legacy 同步走。- 默认不开
plugin-legacy
;当有明确数据或线上反馈,再开,并设检查点(比如三个月复盘一次能不能关)。 - 加一个构建指标:
-legacy
产物总体积和构建时长,别无上限地长。
小结#
@vitejs/plugin-legacy
不是“银弹”,但在该用的时候,它就是最省心的那把伞。别迷信“兼容一切”,也别放任“谁爱挂谁挂”。拿数据说话,按需开关,吃该吃的亏,省能省的力——这才是正解。
附一句大白话:别跟浏览器死磕,我们要做的是“让绝大多数用户顺畅抵达”,剩下的,就看业务值不值了。