浏览器兼容性有何解?plugin-legacy

200 阅读5分钟

先把结论摊开说:plugin-legacy 是给“还吃不动现代 ESM 和新 API”的老浏览器准备的“加餐”。真有用户在用老设备,我们就“花点小钱买安心”;要是你站点 99% 都是新版 Chrome/Safari,劝你忍痛割爱,别为了那 1% 把所有人拖慢。

它到底干啥?

一句话:打两套包,老少咸宜。

  • 现代包(ESM):给现代浏览器吃的,轻巧、快。
  • 祖传包(Legacy):给不支持 ESM 的老家伙吃的,用 SystemJS 加一堆 polyfill,能跑但会重一些。
  • 加载策略:现代浏览器走 <script type="module">,老浏览器走 <script nomodule>;两套互不打扰,谁该吃谁自己认路。

[!tip] 这波属于“多端投喂”:不强迫所有人都吃软饭,但也不饿着老设备。


什么时候该上?三问做决策(别拍脑袋)

  1. 你的用户里有 iOS 12/13、Android 7/8 的长尾吗?
  2. 有嵌套在古早 WebView(例如某些 X5/厂商定制)的业务必须跑吗?
  3. 上线后“白屏/报错”的反馈来自老设备吗?(埋点+日志说话)
  • 三问“有两个 yes”,就开;全都“no”,老老实实省下来,别压榨构建时间。

快速上手:三步走就能跑

安装

npm i -D @vitejs/plugin-legacy
# 或 pnpm add -D @vitejs/plugin-legacy

最小配置

// vite.config.ts
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-legacyindex-legacy 之类产物。
  • 用旧设备/旧 WebView 打开页面,Network 里能看到 -legacy 资源在跑;现代浏览器不会去加载它们。

配置项

  • targets
    • 写法基本等于 Browserslist,决定“老的不老”的分界线。
    • 建议跟你家 .browserslistrc 一致,别前后打架。
  • modernPolyfills
    • true 时,给“差一点就现代”的浏览器(比如 Safari 13)补几个关键 API,避免“一口气噎着”。
  • additionalLegacyPolyfills
    • 点名加一些你用到的 API 垫片,比如 regenerator-runtime/runtime(async/await)、es.array.flat 等。
  • renderLegacyChunks
    • 设为 true(默认)会生成独立的 -legacy 产物,老浏览器才会下载;这也是我们要的“差异装载”。
  • 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 versions
not dead
iOS >= 12
Android >= 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 降级方案。
  • X5/古早 WebView
    • 有时不仅是“是否支持 ESM”,还牵扯安全策略、内置拦截。准备一个“纯静态降级版”兜底,别硬杠。

排查套路:怎么确认 legacy 真生效了?

  • Network 面板:旧设备加载了 polyfills-legacyindex-legacy?OK。
  • Elements/Source:能看到注入的 nomodule 脚本?OK。
  • 控制变量:把 targets 暂时改保守一点,看旧设备是否“起死回生”,能迅速验证思路。

和团队怎么对齐?(一页纸共识)

  • 把用户浏览器分布拉出来(Analytics/埋点),定一个“可解释”的最低版本线。
  • .browserslistrc 一处生效:Autoprefixer、plugin-legacy 同步走。
  • 默认不开 plugin-legacy;当有明确数据或线上反馈,再开,并设检查点(比如三个月复盘一次能不能关)。
  • 加一个构建指标:-legacy 产物总体积和构建时长,别无上限地长。

小结

@vitejs/plugin-legacy 不是“银弹”,但在该用的时候,它就是最省心的那把伞。别迷信“兼容一切”,也别放任“谁爱挂谁挂”。拿数据说话,按需开关,吃该吃的亏,省能省的力——这才是正解。

附一句大白话:别跟浏览器死磕,我们要做的是“让绝大多数用户顺畅抵达”,剩下的,就看业务值不值了。