微前端概述与方案对比
微前端是组织问题的解,不是技术问题的解。 只有当「多个团队要独立发布、又必须拼成一个页面」时,它才划算;否则它带来的复杂度远超收益。
一、什么时候该用(先看这一节)
| 场景 | 该用微前端吗 |
|---|---|
| 10 人以下一个团队、一个仓库 | ❌ 别用,路由级代码分割就够 |
| 大项目要按业务域拆给多个团队,各自独立发版 | ✅ 典型场景 |
| 老系统要逐步替换(Vue2 里嵌 Vue3 / React) | ✅ 增量迁移的常用手段 |
| 只是页面多、想拆包 | ❌ 用懒加载 + 分包 |
| 只想复用几个组件 | ❌ 用 npm 包 / 组件库 |
一条判断线:如果「不同模块的发布节奏」不冲突,就不需要微前端。
二、四种方案的原理
| 方案 | 代表 | 隔离方式 | 加载方式 | 核心代价 |
|---|---|---|---|---|
| iframe | 原生 | 浏览器级(进程/文档级) | iframe src | 通信难、弹窗/滚动受限、首屏多一次文档加载 |
| Web Components | 原生 | Shadow DOM(样式)+ JS 仍共享 | 自定义元素 | 兼容与生态、样式穿透麻烦 |
| qiankun(single-spa 封装) | 蚂蚁 | Proxy 沙箱 + 样式隔离 | 拦截路由,fetch HTML+JS 执行 | 需基座改造、子应用要适配生命周期 |
| Module Federation | Webpack 5 | 无(同一运行时) | 运行时加载远程 chunk | 强绑 Webpack/Rspack,依赖版本要约定 |
qiankun 做了什么
- 路由劫持:基座监听 URL 变化,匹配到子应用就加载;
- HTML Entry:fetch 子应用的 HTML,解析出 JS/CSS 再执行(子应用不用改打包方式);
- JS 沙箱:给子应用的全局变量一个「隔离的 window」;
- 样式隔离:给子应用样式加选择器前缀,或用 Shadow DOM;
- 生命周期:
bootstrap / mount / unmount。
JS 沙箱的两种实现:
| 方式 | 原理 | 特点 |
|---|---|---|
| 快照沙箱(Legacy) | 激活时记录 window 快照,卸载时还原 | 只能有一个子应用运行 |
| Proxy 沙箱(现代) | 用 Proxy 包一个假 window,赋值写进自己的 map | 支持多实例并存 |
Module Federation 做了什么
js
// 远程应用(remote)暴露模块
new ModuleFederationPlugin({
name: 'shop',
filename: 'remoteEntry.js',
exposes: { './Cart': './src/Cart.jsx' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
// 宿主(host)消费
new ModuleFederationPlugin({
remotes: { shop: 'shop@https://cdn.example.com/remoteEntry.js' },
shared: { react: { singleton: true } },
})// 远程应用(remote)暴露模块
new ModuleFederationPlugin({
name: 'shop',
filename: 'remoteEntry.js',
exposes: { './Cart': './src/Cart.jsx' },
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
})
// 宿主(host)消费
new ModuleFederationPlugin({
remotes: { shop: 'shop@https://cdn.example.com/remoteEntry.js' },
shared: { react: { singleton: true } },
})关键在 shared: { singleton: true }:保证 React 只有一份实例,否则 hooks 会报「Invalid hook call」。
三、最小落地示例(qiankun)
js
// 基座
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8081',
container: '#subapp',
activeRule: '/vue',
props: { token: getToken() }, // 传给子应用的 props
},
])
start({ sandbox: { experimentalStyleIsolation: true } })// 基座
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8081',
container: '#subapp',
activeRule: '/vue',
props: { token: getToken() }, // 传给子应用的 props
},
])
start({ sandbox: { experimentalStyleIsolation: true } })js
// 子应用:导出生命周期(Vite 项目需用 vite-plugin-qiankun)
export async function bootstrap() {}
export async function mount(props) { render(props.container) }
export async function unmount() { destroy() }// 子应用:导出生命周期(Vite 项目需用 vite-plugin-qiankun)
export async function bootstrap() {}
export async function mount(props) { render(props.container) }
export async function unmount() { destroy() }四、五个常见坑
| 坑 | 现象 | 解法 |
|---|---|---|
| 弹窗挂到 body | 子应用弹窗样式丢了 | 弹窗挂到子应用容器内,或用 experimentalStyleIsolation |
| 公共依赖重复加载 | 首屏体积翻倍 | MF 的 shared / 基座 externals + CDN |
| 路由冲突 | 子应用路由跳回基座 | 子应用统一用 activeRule 前缀作为 base |
| 全局样式污染 | 子应用 reset 影响基座 | Shadow DOM 或样式前缀 |
| 重复初始化 | 来回切换内存涨 | 在 unmount 里清定时器、解绑事件、销毁实例 |
五、选型决策树
需要多团队独立发布吗?
├─ 不需要 → 用懒加载/分包,别上微前端
└─ 需要
├─ 技术栈要混跑、且要强隔离(老系统迁移)?
│ ├─ 是 → qiankun(成熟)或无界(隔离更强)
│ └─ 否 → 继续
├─ 统一用 Webpack/Rspack、想共享依赖、去中心化?
│ ├─ 是 → Module Federation
│ └─ 否 → 路由分发 + 独立部署
└─ 只需要嵌一个独立页面(不要求 SPA 体验)?
└─ 是 → iframe(最省事,别嫌它土)需要多团队独立发布吗?
├─ 不需要 → 用懒加载/分包,别上微前端
└─ 需要
├─ 技术栈要混跑、且要强隔离(老系统迁移)?
│ ├─ 是 → qiankun(成熟)或无界(隔离更强)
│ └─ 否 → 继续
├─ 统一用 Webpack/Rspack、想共享依赖、去中心化?
│ ├─ 是 → Module Federation
│ └─ 否 → 路由分发 + 独立部署
└─ 只需要嵌一个独立页面(不要求 SPA 体验)?
└─ 是 → iframe(最省事,别嫌它土)六、更温和的演进路径
大多数项目其实应该按这个顺序演进,而不是一上来就微前端:
单体 → 模块化目录(按业务域分)→ npm 包拆公共库 → 路由级代码分割
→ 多仓库 + 独立部署(路由分发)→ 微前端(最后才考虑)单体 → 模块化目录(按业务域分)→ npm 包拆公共库 → 路由级代码分割
→ 多仓库 + 独立部署(路由分发)→ 微前端(最后才考虑)七、速查
| 问题 | 答案 |
|---|---|
| 微前端解决什么问题 | 多团队独立发布 + 拼成一个应用 |
| 什么情况不该用 | 单团队、发布节奏不冲突 |
| qiankun 的沙箱怎么做 | Proxy 包裹 window,赋值写进自己的 map |
| 快照沙箱 vs Proxy 沙箱 | 前者只能单实例,后者支持多实例 |
| MF 最重要的配置 | shared: { react: { singleton: true } } |
| iframe 最大的问题 | 通信与弹窗/滚动体验,不是隔离 |
| 最容易被忽略的坑 | unmount 时没清定时器和事件监听 |
Cao Note