微前端隔离边界:qiankun 与 Module Federation 的取舍之道

发布时间:2026/7/25 12:28:00

微前端隔离边界:qiankun 与 Module Federation 的取舍之道 微前端隔离边界qiankun 与 Module Federation 的取舍之道一、多团队协作下的沙箱困境JS 隔离与样式串扰的实战痛点微前端落地的真实痛点不在框架选型而在隔离边界。一个电商首页同时加载商品、营销、评论三个子应用任何一个子应用的全局变量污染、样式覆盖、路由冲突都会让整页白屏。最典型的三类故障。第一是 JS 全局污染子应用 A 往 window 挂了__config__子应用 B 也挂了后挂的覆盖先挂的A 读取配置拿到 B 的数据。第二是样式串扰子应用 A 用了 antd v4B 用了 antd v5两者的.ant-btn类名冲突按钮样式错乱。第三是资源泄漏子应用卸载后定时器、事件监听、WebSocket 未清理内存持续增长直到页面卡死。qiankun 与 Module Federation下称 MF是当前两种主流方案。前者走运行时沙箱路线后者走构建时共享路线。两者的隔离哲学截然不同取舍也不同。理解边界才能避免“选了框架却没解决问题”。二、Proxy 沙箱与构建时共享qiankun 与 MF 的隔离机理对比qiankun 的隔离核心是 Proxy 沙箱。子应用挂载时qiankun 为其创建一个 fake window所有全局访问被 Proxy 拦截。qiankun 运行时沙箱 ┌────────────────────────────────────────────────────────┐ │ 主应用 window真实 │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 子应用A沙箱 │ │ 子应用B沙箱 │ │ │ │ fakeWindow(A) │ │ fakeWindow(B) │ │ │ │ Proxy 拦截: │ │ Proxy 拦截: │ │ │ │ - 读: 优先自身 │ │ - 读: 优先自身 │ │ │ │ - 写: 记录变更 │ │ - 写: 记录变更 │ │ │ │ 卸载时回滚 │ │ 卸载时回滚 │ │ │ └─────────────────┘ └─────────────────┘ │ │ │ │ 样式隔离: │ │ - strictStyleIsolation: Shadow DOM │ │ - experimentalStyleIsolation: scope 改写 │ └────────────────────────────────────────────────────────┘ Module Federation 构建时共享 ┌────────────────────────────────────────────────────────┐ │ 宿主Host │ │ - 声明 shared: { react, antd } │ │ - 运行时提供共享模块实例 │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ 远程 Remotes │ │ 远程 Remotes │ │ │ │ (独立构建) │ │ (独立构建) │ │ │ │ 消费 host 的 │ │ 消费 host 的 │ │ │ │ react 实例 │ │ react 实例 │ │ │ └─────────────────┘ └─────────────────┘ │ └────────────────────────────────────────────────────────┘关键差异在于隔离时机。qiankun 在运行时拦截子应用代码不变MF 在构建时协商子应用必须声明依赖。前者是“黑盒隔离”后者是“白盒共享”。qiankun 子应用加载时序: 主应用 → import-html-entry 拉子应用 JS → 包装进 Proxy 沙箱执行 → 拦截 window 写入, 记录到 sandboxMap → 子应用 mount(el) 卸载时: → 子应用 unmount(el) → 遍历 sandboxMap, 删除新增的全局变量 → 移除动态插入的 style 标签 MF 模块加载时序: 宿主 → __webpack_init_sharing__ 初始化共享作用域 → 远程入口 remoteEntry.js 注入 → 远程模块通过 __webpack_share_scopes__ 查找共享依赖 → 命中则复用宿主实例, 未命中则按 fallback 加载隔离维度qiankunModule FederationJS 隔离方式Proxy 沙箱运行时共享作用域构建时样式隔离Shadow DOM / scope无内置方案靠 CSS Modules依赖复用不复用各子应用自带显式声明 shared可复用技术栈约束子应用需改造生命周期子应用需 webpack 5路由隔离需手动配置 base天然按模块加载通信机制props / Actions 通信共享模块 / 自定义事件三、生产级微前端落地子应用加载、错误兜底与样式隔离生产环境里微前端必须解决三件事加载失败兜底、样式严格隔离、资源泄漏防御。先看 qiankun 的生产配置// src/micro/qiankun-setup.ts // qiankun 主应用注册含错误兜底、样式隔离、生命周期监控 import { registerMicroApps, start, addGlobalUncaughtError, } from qiankun; interface SubAppConfig { name: string; entry: string; activeRule: string; container: string; props?: Recordstring, unknown; } const SUB_APPS: SubAppConfig[] [ { name: product, entry: //product.example.com, activeRule: /product, container: #sub-app-container, props: { apiBase: import.meta.env.VITE_API_BASE }, }, { name: marketing, entry: //marketing.example.com, activeRule: /marketing, container: #sub-app-container, }, ]; registerMicroApps(SUB_APPS, { beforeLoad: [ async (app) { // 加载前校验入口可达性提前失败优于加载中白屏 const ok await checkEntryReachable(app.entry); if (!ok) { throw new Error(sub-app entry unreachable: ${app.entry}); } console.info([qiankun] beforeLoad: ${app.name}); }, ], afterUnmount: [ async (app) { // 卸载后强制清理残留定时器与事件监听 purgeSandboxLeakage(app.name); }, ], }); start({ prefetch: true, // 预加载未激活的子应用 sandbox: { // Shadow DOM 兼容性差会破坏 antd 的 portal 挂载 strictStyleIsolation: false, // scope 改写兼容性更好生产推荐 experimentalStyleIsolation: true, }, excludeAssetFilter: (url) { // 排除第三方 SDK 的脚本避免被沙箱包装导致异常 return /sentry|googletagmanager/.test(url); }, }); // 全局错误兜底子应用加载失败时展示降级 UI addGlobalUncaughtError((event) { if (event?.type unhandledrejection) { const reason event.reason; if ( reason?.message?.includes(died in status LOADING_SOURCE_CODE) ) { renderFallback( #sub-app-container, 子应用加载失败请稍后重试 ); } } }); // 入口可达性预检HEAD 请求探测3 秒超时 async function checkEntryReachable(entry: string): Promiseboolean { const controller new AbortController(); const timer setTimeout(() controller.abort(), 3000); try { const resp await fetch(entry, { signal: controller.signal, method: HEAD, }); // 部分服务器禁 HEAD 返回 405也视为可达 return resp.ok || resp.status 405; } catch { return false; } finally { clearTimeout(timer); } } // 清理子应用挂载的全局定时器约定注册到 __APP_TIMERS__ function purgeSandboxLeakage(appName: string): void { const timers (window as any).__APP_TIMERS__?.[ appName ] as number[] | undefined; timers?.forEach((id) { clearInterval(id); clearTimeout(id); }); } function renderFallback( selector: string, msg: string ): void { const el document.querySelector(selector); if (el) { el.innerHTML div stylepadding:40px;text-align:center${msg}/div; } }关键设计experimentalStyleIsolation用 scope 改写而非 Shadow DOM兼容 portal 场景入口可达性预检卸载后清理定时器全局错误兜底。再看 MF 的生产配置// webpack.config.js (Host) // Module Federation 宿主配置共享依赖 版本协商 单例约束 const { ModuleFederationPlugin } require(webpack).container; module.exports { plugins: [ new ModuleFederationPlugin({ name: shell, remotes: { product: product//product.example.com/remoteEntry.js, marketing: marketing//marketing.example.com/remoteEntry.js, }, shared: { react: { // 全局单例避免多实例导致 hooks 报错 singleton: true, requiredVersion: ^18.2.0, eager: false, // 异步加载降低首屏体积 }, react-dom: { singleton: true, requiredVersion: ^18.2.0, }, antd: { singleton: true, requiredVersion: ^5.12.0, // 版本不匹配时降级到子应用自带而非报错 strictVersion: false, }, }, }), ], };// src/micro/mf-loader.ts // MF 远程模块加载带超时、回退与错误边界 import React, { Suspense, lazy, type ComponentType, } from react; const LOAD_TIMEOUT_MS 8000; // 远程加载超时阈值 export function loadRemote( scope: string, module: string, fallback: ComponentType ): ComponentType { const LazyComp lazy(async () { const controller new AbortController(); const timer setTimeout( () controller.abort(), LOAD_TIMEOUT_MS ); try { // 初始化共享作用域必须在加载远程模块前完成 await __webpack_init_sharing__(default); const container (window as any)[scope]; if (!container) { throw new Error(remote container not found: ${scope}); } await container.init(__webpack_share_scopes__.default); const factory await container.get(module); if (!factory) { throw new Error( remote module not found: ${scope}/${module} ); } return { default: factory() as ComponentType }; } catch (err) { // 加载失败回退到本地 fallback 组件避免整页白屏 console.error( [mf-loader] load failed ${scope}/${module}:, err ); return { default: fallback }; } finally { clearTimeout(timer); } }); return function RemoteWrapper(props: any) { return ( Suspense fallback{div加载中.../div} LazyComp {...props} / /Suspense ); }; } // TypeScript 全局声明避免编译报错 declare const __webpack_init_sharing__: ( scope: string ) Promisevoid; declare const __webpack_share_scopes__: { default: Recordstring, unknown; };关键设计singleton: true避免 React 多实例、strictVersion: false允许版本降级、加载失败回退到本地 fallback 组件、8 秒超时防止永久挂起。四、隔离强度与构建耦合两种方案的代价与边界两种方案各有代价不存在银弹。qiankun 的代价。第一是 Proxy 沙箱有性能开销每次 window 访问都走代理高频全局访问的场景会有 5 到 10% 性能损耗。第二是experimentalStyleIsolation的 scope 改写对动态插入的样式如 antd 的 css-in-js支持不全需要额外配置。第三是子应用必须暴露bootstrap、mount、unmount生命周期对老应用有改造成本。第四是不支持共享依赖每个子应用自带 React体积冗余。MF 的代价更隐蔽。第一是构建时耦合宿主与子应用必须用 webpack 5 以上且 shared 配置必须对齐否则运行时协商失败。第二是版本漂移风险strictVersion: false虽然能降级但降级后子应用用自己的依赖可能行为不一致。第三是无样式隔离必须依赖 CSS Modules 或 Tailwind 等方案否则样式串扰无解。第四是远程模块加载失败时整个路由白屏必须有 ErrorBoundary 兜底。适用边界明确如下。qiankun 适合技术栈异构Vue 加 React 混用、子应用需要强隔离、团队自治程度高。MF 适合技术栈统一全 React 或全 Vue、依赖复用优先、追求首屏体积、构建链可控。禁用场景也要说清。qiankun 不适合子应用大量使用 Web Worker 或 SharedArrayBuffer沙箱无法隔离二进制共享内存、对首屏性能极敏感沙箱初始化有百毫秒级开销。MF 不适合子应用来自外部第三方无法约束 webpack 版本、需要严格安全隔离MF 无沙箱子应用代码与宿主同源执行。五、总结微前端隔离的本质是在“隔离强度”与“复用效率”之间做取舍。qiankun 选运行时沙箱隔离强但复用弱MF 选构建时共享复用强但隔离弱。没有折中方案只有按场景选边。落地步骤建议如下。第一步评估技术栈异构程度全栈统一优先 MF异构优先 qiankun。第二步qiankun 方案下启用 experimentalStyleIsolation 而非 Shadow DOM配置全局错误兜底与卸载清理。第三步MF 方案下shared 全部声明 singletonstrictVersion 按依赖重要程度分级配置。第四步无论哪种方案都必须为远程模块加载失败准备 ErrorBoundary 与降级 UI。第五步建立子应用健康度面板监控加载耗时、失败率、内存增长三项指标发现泄漏及时干预。微前端不是架构升级的终点而是团队规模与业务复杂度达到一定阈值后的必然选择。隔离边界的设计质量决定了这套架构能否长期演进。选型时宁可保守不可冒进。

相关新闻