微前端概念
微前端是把多个可以独立开发、独立部署的前端应用,组合成一个对用户而言完整的产品的架构方式。这篇讲清它解决什么问题、什么时候该用和不该用、落地时必须面对的几个技术问题,以及几种主要实现方式的差异。具体的 Module Federation 实践见微前端方案与实践。
微前端的思路借鉴了后端的微服务:把一个大型前端应用按业务拆成多个较小的应用,每个应用由一个团队负责,可以独立开发、测试、部署,运行时再聚合成一个整体。
需要明确两点:
- 微前端是架构风格,不是某个具体技术。 它可以用 iframe、路由分发、模块联邦等不同方式实现,配套的还有脚手架、规范和约定。
- “独立部署”是核心特征。 如果拆出来的部分必须一起构建、一起发布,那只是模块化或 monorepo,算不上微前端。
代码组织方式的演进
Section titled “代码组织方式的演进”多个前端应用之间共享代码和协作,常见的几种组织方式:
| 方式 | 做法 | 问题 |
|---|---|---|
| 单体应用 | 一个仓库、一个应用,所有功能在一起 | 规模变大后构建慢,团队互相影响,技术栈难以升级 |
| npm 包共享 | 公共部分发布成 npm 包,各应用安装使用 | 更新要发版、各应用逐一升级重新部署,版本容易分裂 |
| monorepo | 多个包放在一个仓库中统一管理 | 改善了代码共享,但应用仍需一起构建或协调发布 |
| 微前端 | 各应用独立部署,运行时组合 | 带来运行时集成的复杂度 |
微前端带来的好处
Section titled “微前端带来的好处”- 独立部署:一个子应用上线不需要其他应用重新构建发布,发布频率和风险都降低。
- 团队自治:按业务边界划分团队,每个团队拥有从开发到上线的完整链路。
- 增量升级:老系统可以逐步替换,新功能用新技术栈写成子应用接入,不必一次性重写。
- 技术栈无关:不同子应用可以使用不同框架。这是能力而不是目标,同一产品里技术栈太杂会提高维护成本。
- 代码库更小:每个应用只包含自己的业务,构建更快,理解成本更低。
适用与不适用场景
Section titled “适用与不适用场景”适用:
- 大型系统由多个团队共同维护,团队之间的发布节奏互相拖累。
- 需要把多个已有的独立系统整合到一个入口(如中后台、工作台类产品)。
- 老项目技术栈过旧,需要渐进式迁移。
- 平台型产品需要接入其他团队甚至外部提供的功能模块。
不适用:
- 小团队、中小型应用。拆分带来的运行时集成、联调、部署和监控成本,会超过它带来的收益。
- 各部分之间交互非常紧密,频繁共享状态。拆分后会产生大量跨应用通信,边界划分本身就有问题。
- 对首屏性能要求极高的页面。加载多个独立应用的开销很难完全消除。
无论选择哪种实现方式,都要回答下面几个问题。
主应用(也叫基座、容器应用)如何获取和挂载子应用:
- 按什么入口加载:加载子应用的 HTML(HTML Entry,解析出其中的 JS、CSS),还是加载约定好的 JS 入口文件(JS Entry)。
- 生命周期约定:子应用需要暴露挂载、卸载等函数,主应用在合适的时机调用。
- 预加载:在空闲时提前加载其他子应用的资源,减少切换时的等待。
多个应用运行在同一个页面、同一个 window 下,可能互相污染:
- 子应用修改或新增全局变量,卸载后残留,影响其他应用。
- 子应用注册的事件监听、定时器在卸载后没有清理。
常见的沙箱实现:
| 方式 | 原理 | 特点 |
|---|---|---|
| 快照沙箱 | 挂载前记录 window 的属性快照,卸载时对比并还原 |
实现简单,但同一时间只能运行一个子应用 |
| Proxy 沙箱 | 用 Proxy 为每个子应用创建一个假的 window,写操作落在代理对象上 |
支持多个子应用同时运行,依赖 Proxy |
| iframe 天然隔离 | 子应用运行在独立的浏览上下文中 | 隔离最彻底,但通信和体验受限 |
需要注意,基于 Proxy 的沙箱是“尽量隔离”,并不是安全边界:子应用通过某些方式仍可能访问到真实的全局对象。子应用来自不受信任的第三方时,应使用 iframe 隔离。
各应用的 CSS 都作用于同一个文档,类名冲突、全局样式覆盖很常见。处理方式:
- 约定前缀:每个子应用的类名加统一前缀,或使用 CSS Modules、CSS-in-JS 生成唯一类名。成本最低,依赖团队遵守。
- 运行时加作用域:给子应用的样式规则自动添加属性选择器前缀,限定在子应用的容器内。
- Shadow DOM:把子应用挂在 Shadow DOM 中,样式天然隔离。但弹窗、下拉框等挂载到
document.body的组件会跑出 Shadow DOM,导致样式丢失,需要额外处理。 - 子应用卸载时移除其样式:避免已卸载应用的样式继续生效。
- URL 和路由参数:最松耦合,适合传递页面级状态。
- 自定义事件或发布订阅:基于
CustomEvent或主应用提供的事件总线。 - 主应用下发的共享状态:主应用通过 props 把用户信息、权限、全局状态传给子应用。
原则是通信越少越好、越显式越好。子应用之间如果需要大量通信,往往说明拆分的边界不对。
- 主应用负责一级路由,决定当前激活哪个子应用;子应用负责自己内部的二级路由。
- 子应用的路由需要配置基础路径(base),并处理好浏览器前进后退时主、子应用路由的同步。
- 多个子应用同时监听路由变化时,要避免互相干扰。
每个子应用都打包一份 React、Vue 或组件库,会造成重复下载,同一个库存在多个实例时还可能出现问题(比如 React 的 Hooks 要求只有一个 React 实例)。处理方式:
- 通过
externals把公共库排除在构建之外,主应用以全局变量的形式提供。要求各应用使用相同版本。 - 使用 Module Federation 的
shared配置,在运行时协商共享依赖的版本。 - 公共组件和工具通过 npm 包或远程模块共享。
此外还有一些工程问题:子应用资源地址的配置与发现(配置中心)、各应用独立部署后的版本兼容、统一的监控与埋点、本地开发时的联调方式。
主要实现方式
Section titled “主要实现方式”iframe
Section titled “iframe”最早也最简单的方案,每个子应用放在一个 <iframe> 中。
- 优点:隔离最彻底,JS、CSS、全局对象完全独立;接入成本几乎为零,任何技术栈都能用。
- 缺点:
- 子应用的 URL 状态与浏览器地址栏不同步,刷新后状态丢失,前进后退行为不符合预期。
- 弹窗、遮罩只能覆盖在 iframe 区域内,无法覆盖整个页面。
- 应用间通信只能通过
postMessage,需要处理序列化和来源校验。 - 每个 iframe 都是独立的浏览上下文,内存占用大,加载慢,公共依赖无法共享。
适合对隔离要求高、交互简单的场景,比如嵌入第三方系统。
single-spa:路由分发
Section titled “single-spa:路由分发”single-spa 定义了子应用的生命周期(bootstrap、mount、unmount),主应用根据当前路由决定激活哪些子应用:
import { registerApplication, start } from 'single-spa';
registerApplication({ name: 'order', // 常见做法:用 SystemJS 配合 import map 把 'order' 解析为子应用的入口地址 app: () => System.import('order'), activeWhen: ['/order'],});
start();single-spa 只负责“什么时候加载和挂载哪个应用”,不提供 JS 沙箱和样式隔离,子应用的加载方式也需要自行配置(常配合 SystemJS 或 import map)。它更像一个底层框架。
qiankun
Section titled “qiankun”qiankun 基于 single-spa 封装,补齐了开箱即用的能力:
- HTML Entry:直接填写子应用的访问地址,框架拉取 HTML 并解析出 JS 和 CSS,子应用基本不需要改构建方式。
- JS 沙箱:默认使用 Proxy 沙箱,不支持
Proxy的环境退化为快照沙箱。 - 样式隔离:提供基于 Shadow DOM 的严格隔离和基于选择器前缀的作用域隔离两种选项。
- 预加载、应用间通信等辅助能力。
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([ { name: 'order', entry: '//localhost:7100', container: '#subapp-container', activeRule: '/order', },]);
start();子应用需要导出生命周期函数,并把构建产物配置成 UMD 格式等,以便主应用获取。需要注意,qiankun 加载子应用 JS 的方式与原生 ES Module 不完全兼容,使用 Vite 构建的子应用通常需要额外插件或改造。
Module Federation
Section titled “Module Federation”Module Federation(模块联邦)由 webpack 5 引入,思路与前几种不同:它不以“应用”为单位集成,而是让一个构建产物在运行时直接引用另一个独立部署的构建产物中暴露的模块,并自动协商共享依赖。
- 粒度更灵活:可以共享一个按钮组件,也可以共享整个页面。
- 没有中心化的“基座”要求,每个应用既可以暴露模块(remote),也可以消费其他应用的模块(host)。
- 依赖共享是一等能力,React、Vue 等可以在多个应用间只加载一份。
- 不提供 JS 沙箱和样式隔离,各应用运行在同一个上下文中,需要靠约定避免冲突。
- 多个应用需要使用支持模块联邦的构建工具;Rspack 原生支持,Vite 可以通过插件支持。
Web Components
Section titled “Web Components”用浏览器原生的 Custom Elements 把子应用封装成自定义标签,配合 Shadow DOM 隔离样式:
class OrderApp extends HTMLElement { connectedCallback() { const root = this.attachShadow({ mode: 'open' }); mountOrderApp(root); // 子应用渲染到 Shadow Root }
disconnectedCallback() { unmountOrderApp(); }}
customElements.define('order-app', OrderApp);<order-app user-id="42"></order-app>- 优点:浏览器标准,与框架无关;生命周期由
connectedCallback/disconnectedCallback天然提供;Shadow DOM 提供样式隔离。 - 缺点:没有 JS 隔离;Shadow DOM 带来的弹层样式、事件重定向、全局样式无法穿透等问题需要逐一处理;属性只能传字符串,复杂数据要通过 JS 属性传递。
实际使用中,Web Components 更多作为子应用的“封装外壳”,与其他加载方案组合使用。也有一些框架把 Web Components 和 iframe 结合起来,用 iframe 承载 JS 运行环境实现隔离,用 Web Components 承载 DOM 渲染解决 iframe 的弹窗和路由问题。
| 方案 | 集成粒度 | JS 隔离 | 样式隔离 | 依赖共享 | 子应用改造成本 | 主要问题 |
|---|---|---|---|---|---|---|
| iframe | 应用 | 完全隔离 | 完全隔离 | 无 | 极低 | 体验割裂、通信不便、性能开销大 |
| single-spa | 应用 | 无 | 无 | 需自行处理 | 中:导出生命周期,配置加载方式 | 能力偏底层,需要自己补齐 |
| qiankun | 应用 | Proxy / 快照沙箱 | 可选 | 需自行处理 | 中:导出生命周期,调整构建格式 | 沙箱有边界情况,对 ESM 构建支持需额外处理 |
| Module Federation | 模块到应用均可 | 无 | 无 | 内置,运行时协商版本 | 中:调整构建配置 | 依赖构建工具支持,版本管理要求高 |
| Web Components | 组件到应用 | 无 | Shadow DOM | 需自行处理 | 中 | Shadow DOM 兼容细节多 |
- 微前端是让多个独立开发、独立部署的应用组合成一个产品的架构风格,核心特征是独立部署。
- 它解决的是组织和发布上的耦合,小团队、小应用、交互紧密的系统不适合引入。
- 落地需要解决应用加载、JS 沙箱、样式隔离、通信、路由、公共依赖等问题。
- iframe 隔离最好但体验差;single-spa 提供路由分发的底层能力;qiankun 在其上补齐沙箱、样式隔离和 HTML Entry;Module Federation 以模块为粒度共享代码和依赖;Web Components 提供标准化的封装方式。