|
| 1 | +# Legacy 构建与兼容性指南(Vite 与 Webpack) |
| 2 | + |
| 3 | +目的 |
| 4 | +- 当你遇到旧浏览器(例如低版本 iOS / Safari)导致页面白屏或运行时报错的兼容性问题时,应考虑两类方案: |
| 5 | + 1. 在源码层修复(首选,必要时必须做的,比如 RegExp 语法不可能被 polyfill) |
| 6 | + 2. 在构建/部署层使用 legacy 构建(为旧浏览器生成降级 bundle + polyfills) |
| 7 | + |
| 8 | +重要提醒(核心) |
| 9 | +- 一些问题(例如使用正则 lookbehind `(?<!...)`)是 JS 引擎级语法问题:无法用 polyfill 在旧引擎中“添加”正则语法支持。换言之,legacy 构建能转换语法和注入 API polyfill,但不能把运行时未实现的正则语法自动转换为等价旧写法。遇到这类问题,应在源码层重写或在运行时做特性检测并 fallback(参见 `docs/IOS-regex-compat.md`)。 |
| 10 | + |
| 11 | +下面文档提供: |
| 12 | +- Vite 的 `@vitejs/plugin-legacy` 用法与 caveats |
| 13 | +- Webpack 下实现 modern/legacy 差异化构建的常见模式 |
| 14 | +- polyfills 管理(`core-js` / `regenerator-runtime` / 浏览器目标) |
| 15 | +- 测试与 CI 建议 |
| 16 | + |
| 17 | +1) Vite(推荐用于 Vite 项目) |
| 18 | + |
| 19 | +- 安装(项目根): |
| 20 | + |
| 21 | +```bash |
| 22 | +pnpm add -D @vitejs/plugin-legacy |
| 23 | +``` |
| 24 | + |
| 25 | +- 最简单的配置(vite.config.ts) |
| 26 | + |
| 27 | +```ts |
| 28 | +import legacy from '@vitejs/plugin-legacy' |
| 29 | +import { defineConfig } from 'vite' |
| 30 | + |
| 31 | +export default defineConfig({ |
| 32 | + plugins: [ |
| 33 | + legacy({ |
| 34 | + targets: ['defaults', 'iOS >= 11'], |
| 35 | + additionalLegacyPolyfills: ['regenerator-runtime/runtime'], |
| 36 | + // modern polyfilled bundle + legacy (nomodule) bundle -> differential serving |
| 37 | + renderLegacyChunks: true, |
| 38 | + }) |
| 39 | + ] |
| 40 | +}) |
| 41 | +``` |
| 42 | + |
| 43 | +- 解释与建议: |
| 44 | + - `targets` 使用浏览器目标来决定哪些语法/transform 需要降级;根据你需要支持的最低版本设置(例如 `iOS >= 11`)。 |
| 45 | + - `additionalLegacyPolyfills` 可以把某些运行时库插入 legacy bundle;但大多数 API polyfill(例如 `Promise` / `fetch`)应通过 `core-js` 或手动引入更精细地控制。 |
| 46 | + - `renderLegacyChunks: true` 会生成兼容旧浏览器的 chunk(按需 polyfill 注入),并配合 `<script type="module">` / `<script nomodule>` 差异化加载。 |
| 47 | + - plugin-legacy 使用 Babel 来转换语法,但**无法**把正则 lookbehind 之类的引擎级语法“改写”为等价旧写法:如果源码里有不支持的正则语法,仍可能在打包时或运行时报错(取决于是否把该正则放到运行时构造)。因此仍要在源码层修复正则语法。 |
| 48 | + |
| 49 | +- 使用建议流程: |
| 50 | + 1. 尝试用 runtime feature detection(例如 `new RegExp('(?<!a)b')`)判断是否需要改写/延迟构造正则字面量。 |
| 51 | + 2. 为支持低版本平台保留 legacy 构建(plugin-legacy)以覆盖大多数语法 / API 差异。 |
| 52 | + 3. 在 QA/CI 上用旧 iOS Safari(或 BrowserStack)验证构建产物。查看是否有 `SyntaxError: Invalid regular expression` 类型错误(若有,说明源码中有不兼容的正则字面量仍需改写)。 |
| 53 | + |
| 54 | +2) Webpack(适用于基于 Webpack 的项目) |
| 55 | + |
| 56 | +Webpack 没有像 Vite 那样开箱即用的单一插件来生成 differential bundles,但常见方案是:使用 Babel 对代码做两套构建(modern + legacy),并用 `type="module"` / `nomodule` 或不同 HTML 输出来差异化服务。 |
| 57 | + |
| 58 | +- 关键依赖 |
| 59 | + |
| 60 | +```bash |
| 61 | +pnpm add -D @babel/core babel-loader @babel/preset-env core-js regenerator-runtime webpack webpack-cli |
| 62 | +``` |
| 63 | + |
| 64 | +- 简单思路(多配置/多构建) |
| 65 | + - 配置 A(modern):目标现代浏览器(browserslist 可设置),输出 `module` bundle |
| 66 | + - 配置 B(legacy):目标旧浏览器,开启更激进的 `@babel/preset-env` 转换与 `useBuiltIns: 'usage'`,并注入 `core-js` polyfills |
| 67 | + |
| 68 | +示例 `babel.config.js`(用于 legacy 构建) |
| 69 | + |
| 70 | +```js |
| 71 | +module.exports = function (api) { |
| 72 | + const isLegacy = api.env('legacy') |
| 73 | + return { |
| 74 | + presets: [ |
| 75 | + [ |
| 76 | + '@babel/preset-env', |
| 77 | + { |
| 78 | + targets: isLegacy ? { ie: '11', ios: '10' } : { esmodules: true }, |
| 79 | + useBuiltIns: isLegacy ? 'usage' : false, |
| 80 | + corejs: isLegacy ? { version: 3, proposals: false } : undefined, |
| 81 | + }, |
| 82 | + ], |
| 83 | + ], |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +然后在 package.json 中定义两个构建命令(分别以不同的 NODE_ENV/env 来调用 babel): |
| 89 | + |
| 90 | +```json |
| 91 | +{ |
| 92 | + "scripts": { |
| 93 | + "build:modern": "cross-env BABEL_ENV=modern webpack --config webpack.modern.config.js", |
| 94 | + "build:legacy": "cross-env BABEL_ENV=legacy webpack --config webpack.legacy.config.js", |
| 95 | + "build": "pnpm run build:modern && pnpm run build:legacy" |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +- HTML 部署时使用 `type="module"` / `nomodule`: |
| 101 | + |
| 102 | +```html |
| 103 | +<script type="module" src="/assets/app.modern.js"></script> |
| 104 | +<script nomodule src="/assets/app.legacy.js"></script> |
| 105 | +``` |
| 106 | + |
| 107 | +- 注意事项: |
| 108 | + - 两套构建会增加构建复杂度与 CI 时间;但可以确保对旧浏览器的兼容性控制更粒度。 |
| 109 | + - 同样强调:如果源码中包含引擎级不支持的正则字面量(例如 lookbehind),在构建阶段仍可能失败或在旧浏览器上报错 —— 这些必须在源码层修复或用运行时延迟构造。 |
| 110 | + |
| 111 | +3) polyfills 管理建议 |
| 112 | +- 使用 `core-js`(v3)做标准 API polyfill,结合 Babel 的 `useBuiltIns: 'usage'` 自动注入,或手动在入口文件按需 `import 'core-js/stable'`。 |
| 113 | +- 对于 async/await:`regenerator-runtime/runtime`。 |
| 114 | +- 注意不要把不必要的 polyfill注入到 modern bundle;使用 differential serving(或 plugin-legacy)确保最小现代 bundle。 |
| 115 | + |
| 116 | +4) 正则(RegExp)与引擎级语法的特殊说明 |
| 117 | +- 正则语法(例如 lookbehind)是 JS 引擎实现的语言特性,无法用 runtime polyfill 修复。解决办法: |
| 118 | + - 在源码中改写为兼容写法(例如把 `(?<!a)b` 改为 `(^|[^a])b` 或使用字符串检查回调),或 |
| 119 | + - 把潜在会抛错的正则延迟为运行时构造(`new RegExp(...)`)并在运行时做 feature-detection(参考 `docs/IOS-regex-compat.md`),以避免模块加载解析时报错。 |
| 120 | + |
| 121 | +5) 测试与 CI / 预提交检查 |
| 122 | +- 在 CI 中加入: |
| 123 | + - `pnpm run build`(确保 modern+legacy 构建在 CI 执行无异常) |
| 124 | + - 旧版浏览器 smoke tests(可选):用 BrowserStack / SauceLabs 在 iOS 16.2 / Safari 等上做速测,或在 Xcode Simulator 上跑关键页面。 |
| 125 | + - 静态检测脚本:阻止新提交引入诸如 `(?<!` / `(?<=` 的字面量正则(示例脚本见下)。把脚本加入 `package.json` 并在 CI/预提交钩子里运行。 |
| 126 | + |
| 127 | +示例检查脚本(package.json -> scripts) |
| 128 | + |
| 129 | +```json |
| 130 | +{ |
| 131 | + "scripts": { |
| 132 | + "check:regex-advanced": "rg --hidden --glob '!node_modules' --glob '!dist' -n --glob '**/*.{js,ts,vue,jsx,tsx,mjs,cjs}' '\\(\\?<!|\\(\\?<=|\\(\\?<[^=]|\\\\p\\{' || true" |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +6) 快速排错清单(当你看到旧设备白屏或控制台有错误时) |
| 138 | +- 打开控制台,查找 `SyntaxError: Invalid regular expression` 或语法错误;如果是 RegExp 报错,优先检查源码是否含 lookbehind 等高级特性。 |
| 139 | +- 用 `rg '\\(\\?<!|\\(\\?<=|\\\\p\\{'` 在源码中搜索(排除缓存文件与 node_modules)。 |
| 140 | +- 若查明是正则语法导致:在源码中改写或延迟构造;不要仅依赖 legacy 构建来修复此类问题。 |
| 141 | +- 若是其它语法/API:确保 legacy 构建或 Babel preset-env 的 targets 包含你需要支持的版本,并把 polyfill 策略设置为 `usage`(或按需引入)。 |
| 142 | + |
| 143 | +7) 总结要点(简短) |
| 144 | +- legacy 构建(Vite plugin-legacy / Webpack + Babel 两套构建)能解决大量语法与 API 的兼容问题,但不能修复 JS 引擎本身不支持的正则语法(如 lookbehind)。遇到这类问题,必须在源码层改写或使用运行时检测与 fallback。 |
| 145 | +- 推荐流程:优先源码兼容 -> 在构建中添加 legacy 支持 -> 在 CI/QA 上验证旧设备的真实表现 -> 在项目中加入静态检查以防回归。 |
| 146 | + |
| 147 | +附录:参考命令 |
| 148 | + |
| 149 | +```bash |
| 150 | +# Vite 本地打包 (含 legacy) |
| 151 | +pnpm build |
| 152 | + |
| 153 | +# Webpack 两套构建 (示例) |
| 154 | +pnpm run build:modern |
| 155 | +pnpm run build:legacy |
| 156 | + |
| 157 | +# 静态检查(本地快速跑) |
| 158 | +pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n |
| 159 | +``` |
0 commit comments