Files
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

8.0 KiB
Raw Permalink Blame History

Legacy 构建与兼容性指南(Vite 与 Webpack)

目的

  • 当你遇到旧浏览器(例如低版本 iOS / Safari)导致页面白屏或运行时报错的兼容性问题时,应考虑两类方案:
    1. 在源码层修复(首选,必要时必须做的,比如 RegExp 语法不可能被 polyfill)
    2. 在构建/部署层使用 legacy 构建(为旧浏览器生成降级 bundle + polyfills)

重要提醒(核心)

  • 一些问题(例如使用正则 lookbehind (?<!...))是 JS 引擎级语法问题:无法用 polyfill 在旧引擎中“添加”正则语法支持。换言之,legacy 构建能转换语法和注入 API polyfill,但不能把运行时未实现的正则语法自动转换为等价旧写法。遇到这类问题,应在源码层重写或在运行时做特性检测并 fallback(参见 docs/IOS-regex-compat.md)。

下面文档提供:

  • Vite 的 @vitejs/plugin-legacy 用法与 caveats
  • Webpack 下实现 modern/legacy 差异化构建的常见模式
  • polyfills 管理(core-js / regenerator-runtime / 浏览器目标)
  • 测试与 CI 建议
  1. Vite(推荐用于 Vite 项目)
  • 安装(项目根):
pnpm add -D @vitejs/plugin-legacy
  • 最简单的配置(vite.config.ts)
import legacy from '@vitejs/plugin-legacy'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    legacy({
      targets: ['defaults', 'iOS >= 11'],
      additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
      // modern polyfilled bundle + legacy (nomodule) bundle -> differential serving
      renderLegacyChunks: true,
    })
  ]
})
  • 解释与建议:

    • targets 使用浏览器目标来决定哪些语法/transform 需要降级;根据你需要支持的最低版本设置(例如 iOS >= 11)。
    • additionalLegacyPolyfills 可以把某些运行时库插入 legacy bundle;但大多数 API polyfill(例如 Promise / fetch)应通过 core-js 或手动引入更精细地控制。
    • renderLegacyChunks: true 会生成兼容旧浏览器的 chunk(按需 polyfill 注入),并配合 <script type="module"> / <script nomodule> 差异化加载。
    • plugin-legacy 使用 Babel 来转换语法,但无法把正则 lookbehind 之类的引擎级语法“改写”为等价旧写法:如果源码里有不支持的正则语法,仍可能在打包时或运行时报错(取决于是否把该正则放到运行时构造)。因此仍要在源码层修复正则语法。
  • 使用建议流程:

    1. 尝试用 runtime feature detection(例如 new RegExp('(?<!a)b'))判断是否需要改写/延迟构造正则字面量。
    2. 为支持低版本平台保留 legacy 构建(plugin-legacy)以覆盖大多数语法 / API 差异。
    3. 在 QA/CI 上用旧 iOS Safari(或 BrowserStack)验证构建产物。查看是否有 SyntaxError: Invalid regular expression 类型错误(若有,说明源码中有不兼容的正则字面量仍需改写)。
  1. Webpack(适用于基于 Webpack 的项目)

Webpack 没有像 Vite 那样开箱即用的单一插件来生成 differential bundles,但常见方案是:使用 Babel 对代码做两套构建(modern + legacy),并用 type="module" / nomodule 或不同 HTML 输出来差异化服务。

  • 关键依赖
pnpm add -D @babel/core babel-loader @babel/preset-env core-js regenerator-runtime webpack webpack-cli
  • 简单思路(多配置/多构建)
    • 配置 A(modern):目标现代浏览器(browserslist 可设置),输出 module bundle
    • 配置 B(legacy):目标旧浏览器,开启更激进的 @babel/preset-env 转换与 useBuiltIns: 'usage',并注入 core-js polyfills

示例 babel.config.js(用于 legacy 构建)

module.exports = function (api) {
  const isLegacy = api.env('legacy')
  return {
    presets: [
      [
        '@babel/preset-env',
        {
          targets: isLegacy ? { ie: '11', ios: '10' } : { esmodules: true },
          useBuiltIns: isLegacy ? 'usage' : false,
          corejs: isLegacy ? { version: 3, proposals: false } : undefined,
        },
      ],
    ],
  }
}

然后在 package.json 中定义两个构建命令(分别以不同的 NODE_ENV/env 来调用 babel):

{
  "scripts": {
    "build:modern": "cross-env BABEL_ENV=modern webpack --config webpack.modern.config.js",
    "build:legacy": "cross-env BABEL_ENV=legacy webpack --config webpack.legacy.config.js",
    "build": "pnpm run build:modern && pnpm run build:legacy"
  }
}
  • HTML 部署时使用 type="module" / nomodule:
<script type="module" src="/assets/app.modern.js"></script>
<script nomodule src="/assets/app.legacy.js"></script>
  • 注意事项:
    • 两套构建会增加构建复杂度与 CI 时间;但可以确保对旧浏览器的兼容性控制更粒度。
    • 同样强调:如果源码中包含引擎级不支持的正则字面量(例如 lookbehind),在构建阶段仍可能失败或在旧浏览器上报错 —— 这些必须在源码层修复或用运行时延迟构造。
  1. polyfills 管理建议
  • 使用 core-js(v3)做标准 API polyfill,结合 Babel 的 useBuiltIns: 'usage' 自动注入,或手动在入口文件按需 import 'core-js/stable'。
  • 对于 async/await:regenerator-runtime/runtime。
  • 注意不要把不必要的 polyfill注入到 modern bundle;使用 differential serving(或 plugin-legacy)确保最小现代 bundle。
  1. 正则(RegExp)与引擎级语法的特殊说明
  • 正则语法(例如 lookbehind)是 JS 引擎实现的语言特性,无法用 runtime polyfill 修复。解决办法:
    • 在源码中改写为兼容写法(例如把 (?<!a)b 改为 (^|[^a])b 或使用字符串检查回调),或
    • 把潜在会抛错的正则延迟为运行时构造(new RegExp(...))并在运行时做 feature-detection(参考 docs/IOS-regex-compat.md),以避免模块加载解析时报错。
  1. 测试与 CI / 预提交检查
  • 在 CI 中加入:
    • pnpm run build(确保 modern+legacy 构建在 CI 执行无异常)
    • 旧版浏览器 smoke tests(可选):用 BrowserStack / SauceLabs 在 iOS 16.2 / Safari 等上做速测,或在 Xcode Simulator 上跑关键页面。
    • 静态检测脚本:阻止新提交引入诸如 (?<! / (?<= 的字面量正则(示例脚本见下)。把脚本加入 package.json 并在 CI/预提交钩子里运行。

示例检查脚本(package.json -> scripts)

{
  "scripts": {
    "check:regex-advanced": "rg --hidden --glob '!node_modules' --glob '!dist' -n --glob '**/*.{js,ts,vue,jsx,tsx,mjs,cjs}' '\\(\\?<!|\\(\\?<=|\\(\\?<[^=]|\\\\p\\{' || true"
  }
}
  1. 快速排错清单(当你看到旧设备白屏或控制台有错误时)
  • 打开控制台,查找 SyntaxError: Invalid regular expression 或语法错误;如果是 RegExp 报错,优先检查源码是否含 lookbehind 等高级特性。
  • 用 rg '\\(\\?<!|\\(\\?<=|\\\\p\\{' 在源码中搜索(排除缓存文件与 node_modules)。
  • 若查明是正则语法导致:在源码中改写或延迟构造;不要仅依赖 legacy 构建来修复此类问题。
  • 若是其它语法/API:确保 legacy 构建或 Babel preset-env 的 targets 包含你需要支持的版本,并把 polyfill 策略设置为 usage(或按需引入)。
  1. 总结要点(简短)
  • legacy 构建(Vite plugin-legacy / Webpack + Babel 两套构建)能解决大量语法与 API 的兼容问题,但不能修复 JS 引擎本身不支持的正则语法(如 lookbehind)。遇到这类问题,必须在源码层改写或使用运行时检测与 fallback。
  • 推荐流程:优先源码兼容 -> 在构建中添加 legacy 支持 -> 在 CI/QA 上验证旧设备的真实表现 -> 在项目中加入静态检查以防回归。

附录:参考命令

# Vite 本地打包 (含 legacy)
pnpm build

# Webpack 两套构建 (示例)
pnpm run build:modern
pnpm run build:legacy

# 静态检查(本地快速跑)
pnpm dlx rg "\\(\\?<!|\\(\\?<=|\\\\p\\{" --glob "**/*.{js,ts,vue,jsx,tsx,mjs,cjs}" -n