24 · 微前端架构落地
本篇解决什么问题: "该不该上微前端"是选型问题(见 A02),"上了之后怎么让它不翻车"是落地问题——本篇讲后者。 微前端的复杂度几乎全在隔离与通信这两件事的边界处理上。
📌 与 A02 的分工:A02 判断"该不该上",本篇解决"上了怎么落地"。
开篇速览
| 关注点 | 核心问题 |
|---|---|
| 方案选型 | qiankun / Module Federation / iframe / Web Components 怎么选 |
| JS 沙箱 | 子应用的 window 污染怎么办 |
| 样式隔离 | 类名冲突、全局样式互相覆盖 |
| 应用通信 | 怎么传数据而不产生强耦合 |
| 依赖共享 | React/Vue 实例必须单例,否则 Hooks 报错 |
| 路由分发 | 主应用与子应用的路由怎么协调 |
| 预加载 | 切换卡顿怎么缓解 |
一、方案选型:四种路线
| 方案 | 原理 | 隔离性 | 体验 | 适合 |
|---|---|---|---|---|
| iframe | 浏览器原生 | ⭐⭐⭐⭐⭐ 最强 | ❌ 差(弹窗/路由/刷新/通信都别扭) | 完全异构、要绝对隔离的第三方嵌入 |
| qiankun / single-spa | 路由劫持 + 加载子应用 bundle + 沙箱 | ⭐⭐⭐ | ✅ 好 | 多数团队的默认选择 |
| Module Federation(Webpack 5 / Vite) | 构建时声明远程模块,运行时动态共享代码 | ⭐⭐ | ✅ 最好(无刷新、真共享) | 技术栈统一、需要共享组件/依赖 |
| Web Components | 自定义元素 + Shadow DOM | ⭐⭐⭐⭐ | ✅ 好 | 组件级集成(见 A22) |
决策速查
| 场景 | 选 |
|---|---|
| 老系统(jQuery/AngularJS)+ 新系统共存 | qiankun |
| 技术栈统一(都是 React 18 / Vue3),要共享组件与依赖 | Module Federation |
| 要嵌入完全不可信的第三方页面 | iframe |
| 只是想在多个框架间复用一批组件 | Web Components(这不算微前端,别过度设计) |
二、JS 沙箱:隔离 window
问题:两个子应用都往 window 上挂东西,或者都改 window.addEventListener,会互相干扰。
2.1 三种沙箱级别
| 级别 | 做法 | 隔离性 | 性能 |
|---|---|---|---|
| 快照沙箱 | 激活时拍快照,失活时还原 | 弱(单实例场景) | 中 |
| Proxy 沙箱(qiankun 默认) | 用 Proxy 代理一个假的 window,子应用读写都走它 | ✅ 强(多实例并存) | 好 |
| iframe 沙箱 | 用 iframe 的 contentWindow 作为真实隔离环境 | ⭐⭐⭐ 最强 | 差 |
js
// Proxy 沙箱的核心思路(简化)
function createSandbox() {
const rawWindow = window
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
get(t, k) {
if (k in t) return t[k]
return rawWindow[k] // 没改过 → 读真实 window
},
set(t, k, v) {
t[k] = v // 只写到假 window,**不污染全局**
return true
},
})
return { proxy, active() {...}, inactive() {...} }
}2.2 沙箱拦不住什么(务必知道)
| 漏网之鱼 | 说明 |
|---|---|
| 全局事件监听 | window.addEventListener('resize') 需要在子应用卸载时主动移除 |
| 定时器 | setInterval 必须在卸载时清理 |
| DOM 外的全局状态 | document.title、body 上的 class |
| 样式 | JS 沙箱管不了 CSS(见下一节) |
| 第三方 SDK 的副作用 | 某些 SDK 会强行挂全局 |
🎯 子应用的
unmount必须做完整清理:移除监听、清定时器、取消请求、销毁实例。 这是微前端最常见的内存泄漏来源。
三、样式隔离
3.1 三种方案
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| CSS Modules / 原子化 | 构建时加哈希 | ✅ 最彻底、零运行时 | 要求所有子应用都采用 |
| Shadow DOM | attachShadow 真隔离 | ✅ 原生隔离 | 弹窗要挂到 body 就出不来;第三方组件库常不兼容 |
动态作用域(qiankun 的 experimentalStyleIsolation) | 给子应用所有选择器加前缀 | 改动小 | 对 @media、动态插入样式支持有限 |
推荐优先级:CSS Modules/原子化 > Shadow DOM > 动态前缀。
⚠️ 弹窗穿透问题:Shadow DOM 内无法让
Modal渲染到外层body(会脱离 Shadow 树从而丢失样式)。用 Shadow DOM 前先验证你们组件库的弹窗。
3.2 全局样式约定
无论用哪种隔离,都要约定:
- 禁止子应用写
body {}、*这类全局选择器 - 统一重置样式由主应用提供一次,子应用不要各引一份 reset
- 主题 Token 由主应用注入 CSS 变量(见 A14)
四、应用通信:保持松耦合
4.1 三种方式(按推荐度)
① Props / 生命周期参数(首选)
js
// 主应用注册子应用时传入
registerMicroApps([{
name: 'user',
entry: '//localhost:3001',
container: '#subapp',
activeRule: '/user',
props: {
token: () => store.token, // ✅ 传**getter**,保证拿到最新值
onLogout: () => { /* 回调 */ },
routerBase: '/user',
},
}])② 全局状态(props + initGlobalState)
js
// qiankun
const actions = initGlobalState({ user: null })
actions.onGlobalStateChange((state, prev) => { /* 子应用监听 */ })
actions.setGlobalState({ user }) // 主应用修改③ URL / 自定义事件(最松耦合)
js
// 主子应用通过 URL 参数或 CustomEvent 通信 → 完全无直接依赖
window.dispatchEvent(new CustomEvent('cart:change', { detail }))4.2 通信三原则
| 原则 | 说明 |
|---|---|
| 主 → 子 传 getter,不传快照值 | 传值会拿到旧数据;传函数才能实时 |
| 子 → 主 用回调/事件,不直接调主的方法 | 避免子应用编译期依赖主应用 |
| 共享状态尽量少 | 传得越多,耦合越紧,改造越难 |
🎯 跨应用状态共享可以用框架无关的 Zustand(见 A07), 但要放在主应用创建、通过 props 下发,子应用不要自己 new 一个。
五、依赖共享:React/Vue 必须单例
问题:子应用各打包一份 React,会出现:
| 症状 | 原因 |
|---|---|
| "Invalid hook call" | 存在多个 React 实例,Hooks 的规则检查失败 |
| 组件库样式/行为异常 | 多份实例导致 Context 不共享 |
| 体积暴涨 | 每个子应用都打包 React/Vue |
5.1 Module Federation 的共享配置(推荐)
js
// webpack.config.js(主应用与子应用都要配)
new ModuleFederationPlugin({
name: 'host',
remotes: { user: 'user@//localhost:3001/remoteEntry.js' },
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' }, // ✅ singleton 保证单例
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
})5.2 qiankun 场景的做法
把 React/Vue 配成 external,由主应用通过 importmap 或 CDN 提供:
html
<script type="importmap">
{ "imports": { "react": "https://cdn/esm.sh/react@18", "react-dom": "https://cdn/esm.sh/react-dom@18" } }
</script>⚠️ 版本必须一致:主应用 React 18、子应用 React 17,共享后会崩。 统一基础框架版本是微前端的前提,否则别共享。
六、路由分发
主应用负责一级路由,子应用负责二级路由。
js
// 主应用(history 模式)
registerMicroApps([{
name: 'user',
activeRule: '/user', // 主应用匹配到 /user 就激活子应用
props: { routerBase: '/user' } // ✅ 把 base 传给子应用
}])
// 子应用路由要设置 base
createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/user' : '/')要点:
| 要点 | 说明 |
|---|---|
| base 必须传 | 子应用独立运行与嵌入运行用不同 base |
| 404 兜底 | 主应用要处理"子应用路由未匹配"的情况 |
| 子应用间跳转 | 用主应用的 router(history.pushState),不要子应用自己跳 |
七、预加载与性能
js
// qiankun:首屏后预加载其他子应用的静态资源
start({ prefetch: 'all' }) // 或 'all' | true
// 更精细:手动预加载
prefetchApps([{ name: 'user', entry: '//localhost:3001' }])权衡:
| 策略 | 说明 |
|---|---|
prefetch: 'all' | 首屏后加载全部,切换快但浪费流量 |
手动 prefetchApps | 只对高概率访问的子应用预加载(推荐) |
| 不预加载 | 首次切换有白屏 |
其他性能要点:
- 子应用公共依赖共享(见第五节)→ 体积显著下降
- 子应用
mount时避免重型初始化 - 卸载时必须销毁,否则内存只增不减
自检清单
- [ ] 能按场景在 iframe / qiankun / MF / Web Components 之间选型
- [ ] 能说清 Proxy 沙箱的原理,以及它拦不住什么(事件、定时器、样式)
- [ ] 能说出样式隔离的三种方案与优先级,以及 Shadow DOM 的弹窗问题
- [ ] 知道 React/Vue 必须单例共享,以及
singleton: true的配置 - [ ] 能说出子应用
unmount必须清理哪些东西
常见误区
| ❌ 误区 | ✅ 事实 |
|---|---|
| "上了微前端,子应用就完全隔离了" | JS 沙箱管不了样式、全局事件、定时器 |
| "iframe 最安全所以最好" | 体验差(弹窗/路由/通信),只适合第三方嵌入 |
| "Shadow DOM 能隔离一切" | 弹窗挂到 body 会丢失样式 |
| "子应用各装各的 React 没问题" | 会 Invalid hook call,且体积翻倍 |
| "微前端能解决所有耦合问题" | 它解决部署与团队解耦,不解决业务耦合 |
动手练习
- 用 qiankun 搭一个"主应用 + 两个子应用"的最小骨架,验证 JS 沙箱(子应用挂
window.foo,主应用读不到)。 - 故意在子应用里
setInterval且不清理,切换几次后观察内存与控制台(体会泄漏),再补上unmount清理。 - 用 Module Federation 把 React 配成
singleton: true共享,对比共享前后的产物体积。
相关文档
- 该不该上微前端(选型)→ A02 · 前端工程化
- 状态共享方案 → A07 · 状态管理演进与选型
- 跨框架组件复用 → A22 · Web 平台能力
- Token 与设计系统 → A14 · 组件 API 设计与设计系统