Skip to content

微前端概述与方案对比 ​

微前端是组织问题的解,不是技术问题的解。 只有当「多个团队要独立发布、又必须拼成一个页面」时,它才划算;否则它带来的复杂度远超收益。

一、什么时候该用(先看这一节) ​

场景该用微前端吗
10 人以下一个团队、一个仓库❌ 别用,路由级代码分割就够
大项目要按业务域拆给多个团队,各自独立发版✅ 典型场景
老系统要逐步替换(Vue2 里嵌 Vue3 / React)✅ 增量迁移的常用手段
只是页面多、想拆包❌ 用懒加载 + 分包
只想复用几个组件❌ 用 npm 包 / 组件库

一条判断线:如果「不同模块的发布节奏」不冲突,就不需要微前端。

二、四种方案的原理 ​

方案代表隔离方式加载方式核心代价
iframe原生浏览器级(进程/文档级)iframe src通信难、弹窗/滚动受限、首屏多一次文档加载
Web Components原生Shadow DOM(样式)+ JS 仍共享自定义元素兼容与生态、样式穿透麻烦
qiankun(single-spa 封装)蚂蚁Proxy 沙箱 + 样式隔离拦截路由,fetch HTML+JS 执行需基座改造、子应用要适配生命周期
Module FederationWebpack 5无(同一运行时)运行时加载远程 chunk强绑 Webpack/Rspack,依赖版本要约定

qiankun 做了什么 ​

  1. 路由劫持:基座监听 URL 变化,匹配到子应用就加载;
  2. HTML Entry:fetch 子应用的 HTML,解析出 JS/CSS 再执行(子应用不用改打包方式);
  3. JS 沙箱:给子应用的全局变量一个「隔离的 window」;
  4. 样式隔离:给子应用样式加选择器前缀,或用 Shadow DOM;
  5. 生命周期: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 时没清定时器和事件监听