微前端隔离边界: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 拦截。
text
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 在构建时协商,子应用必须声明依赖。前者是"黑盒隔离",后者是"白盒共享"。
text
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 的生产配置:
typescript
// 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 的生产配置:
javascript
// 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,
},
},
}),
],
};
typescript
// 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。第五步,建立子应用健康度面板,监控加载耗时、失败率、内存增长三项指标,发现泄漏及时干预。
微前端不是架构升级的终点,而是团队规模与业务复杂度达到一定阈值后的必然选择。隔离边界的设计质量,决定了这套架构能否长期演进。选型时宁可保守,不可冒进。