微前端隔离边界:qiankun 与 Module Federation 的取舍之道
一、多团队协作下的沙箱困境:JS 隔离与样式串扰的实战痛点
微前端落地的真实痛点,不在框架选型,而在隔离边界。一个电商首页同时加载商品、营销、评论三个子应用,任何一个子应用的全局变量污染、样式覆盖、路由冲突,都会让整页白屏。
最典型的三类故障。第一是 JS 全局污染,子应用 A 往 window 挂了__config__,子应用 B 也挂了,后挂的覆盖先挂的,A 读取配置拿到 B 的数据。第二是样式串扰,子应用 A 用了 antd v4,B 用了 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 加载| 隔离维度 | qiankun | Module Federation |
|---|---|---|
| JS 隔离方式 | 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?: Record<string, 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): Promise<boolean> { 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 style="padding: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 ) => Promise<void>; declare const __webpack_share_scopes__: { default: Record<string, 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 全部声明 singleton,strictVersion 按依赖重要程度分级配置。第四步,无论哪种方案,都必须为远程模块加载失败准备 ErrorBoundary 与降级 UI。第五步,建立子应用健康度面板,监控加载耗时、失败率、内存增长三项指标,发现泄漏及时干预。
微前端不是架构升级的终点,而是团队规模与业务复杂度达到一定阈值后的必然选择。隔离边界的设计质量,决定了这套架构能否长期演进。选型时宁可保守,不可冒进。