前端工程化与微前端问题记录
记录构建工具链、Monorepo、Module Federation 落地时踩过的坑,每个问题都是实际遇到过并解决掉的。
问题一:"本地好的线上坏"
现象:循环依赖的模块级单例在 dev 下初始化一次,prod 下初始化两次。
原因:Vite 双引擎分叉——dev 用 esbuild 预构建(按需 ESM)、prod 用 rollup 打包,两者代码分割策略和循环依赖执行顺序不同;CJS 互操作(default 导出)处理也有差异。
解决:治本靠单引擎统一语义(Vite 6+ Rolldown,与 rollup API 兼容);治标是 CI 加 prod build 后冒烟测试 + ESLint import 规则禁循环依赖。长期修复是消除循环而不是锁定执行顺序——顺序锁只是把炸弹换个位置。
问题二:8000 模块仓库 Vite 冷启动 2 分钟
现象:monorepo 冷启动主要耗在依赖预构建,CI 上每次都要重跑。
原因:预构建缓存(node_modules/.vite)在 lockfile/config 变化时全量失效;bundleless 路线深层依赖还有运行时级联请求瀑布。
解决:CI 缓存预构建产物(key = lockfile hash + vite config hash);大型应用改用惰性构建 + 持久缓存的 bundler 路线(Turbopack/Rspack 只编译当前路由触达的模块,模块图按需构建、HMR 精确失效)。
问题三:改一个底层包,CI 全仓库跑一遍
现象:100 个包的 monorepo,改一个工具包,CI 跑了 40 分钟。
原因:没配置任务依赖图,所有包无条件构建测试。
解决:Turborepo 按 dependsOn: ["^build"] 拓扑执行(^ = 先构建上游),输入 hash(源码+依赖声明+lockfile+环境变量)命中缓存直接回放产物——"只测受影响的" = 图上被标记脏的子树:
{ "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] } } }
版本发布用 Changesets:底层包 minor bump,workspace 内依赖它的包递归自动 bump。循环依赖在 merge 前用 CI 检测拦住(turbo 任务图会直接报环)。
问题四:MF 远程组件把宿主页拖白屏
现象:Module Federation 的 remote CDN 挂了,host 页面白屏等死。
原因:remote 动态加载没有超时和降级;shared singleton 协商失败也没 fail fast,静默跑出双 React 实例。
解决:
async function loadRemote<T>(loader: () => Promise<T>, fallback: () => T): Promise<T> {
try {
return await Promise.race([
loader(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)),
]);
} catch (err) {
report(err);
return fallback(); // 本地兜底组件,remote 挂了不白屏
}
}
版本治理:react/vue 等核心库全部 singleton: true + requiredVersion + strictVersion(协商不满足宁可报错,双实例 Hooks 必炸);工具库放开 singleton 多实例。类型靠 MF 2.0 构建时产出 d.ts 给 host 消费,不发包。
问题五:子应用卸载后定时器还在跑
现象:qiankun 子应用卸载后,第三方 SDK 的定时器和事件监听还在执行,内存持续上涨。
原因:Proxy 沙箱只代理 window 读写,addEventListener/setInterval 靠补丁劫持复原——第三方 SDK 缓存了原始函数引用的就逃逸;样式污染更是另一个治理层,JS 沙箱救不了。
解决:SDK 层强制副作用收集器——mount 时记录新增监听器/定时器/全局变量,unmount 统一清理;样式用 scoping 前缀(Shadow DOM 隔离最强但弹窗挂 body 就穿)。新项目大方向是 Isolated Micro Frontends(浏览器原生隔离),牺牲共享性换真隔离。
延伸:第三方 SDK 缓存 window.fetch 原始引用的缓存逃逸没有完美解,只能白名单审计 + 沙箱补丁尽量收口。
