微前端架构
一、微前端概念
1.1 为什么需要微前端
随着前端应用规模的增长,传统单体前端架构逐渐暴露出诸多问题。当一个应用发展到数百个页面、数十人甚至上百人的团队协作时,代码仓库变得臃肿不堪,构建时间从秒级增长到分钟级,不同业务模块之间耦合严重,任何修改都需要全量回归测试。微前端正是为了解决这些痛点而生。
微前端的核心思想是将前端应用分解为更小、更简单的独立模块,每个模块由独立的团队负责开发、测试和部署,最终在运行时组合成一个完整的产品。这与微服务在后端的理念如出一辙——通过拆分实现关注点分离,通过自治提升交付效率。
典型的业务场景包括:
- 大型后台管理系统:不同业务域(订单、用户、商品、财务)由不同团队开发,各自独立迭代
- B2B 平台整合:通过并购获得多个产品线,需要统一入口但保持各自独立演进
- 渐进式迁移:老旧系统逐步从老框架(如 jQuery、AngularJS)迁移到新框架(如 React、Vue),无需重写全部代码
- 多租户 SaaS 平台:不同租户需要使用不同的功能模块组合,微前端可以灵活编排
1.2 技术选型考量
选择微前端方案时,需要综合评估以下维度:
| 维度 | 说明 |
|---|---|
| 技术栈无关 | 主应用与子应用是否可以使用不同框架(React、Vue、Angular 等) |
| 独立部署 | 子应用是否可以独立构建、独立部署,不依赖主应用 |
| 隔离性 | JS 沙箱和样式隔离的强度,是否会出现全局变量污染 |
| 通信成本 | 应用间通信的复杂度,是否支持双向数据流 |
| 性能开销 | 运行时额外开销,包括加载时间、内存占用 |
| 学习曲线 | 框架本身的学习成本和现有项目的改造成本 |
| 社区生态 | 文档完善度、Issue 响应速度、第三方插件丰富度 |
1.3 拆分原则
微前端的拆分粒度需要仔细权衡,拆得过粗收益有限,拆得过细又会导致通信和管理成本激增。推荐的拆分原则如下:
- 按业务域拆分:每个子应用对应一个完整的业务领域,如用户管理、订单管理、数据分析等。这是最自然的拆分方式,团队边界清晰。
- 独立交付能力:每个子应用从开发、测试到部署的全生命周期应独立完成,不依赖其他模块的发布节奏。
- 共享基础设施:UI 组件库、工具函数、API 客户端等公共模块应当以 npm 包或共享库的形式复用,避免各自重复实现。
- 数据自治:每个子应用拥有自己的数据层(状态管理、API 请求),不直接操作其他子应用的数据。
- 渐进式采用:不要求一次性全部迁移,允许新旧系统共存,逐步替换。
1.4 通信机制
微前端中的应用间通信是架构设计的核心难点之一。常见的通信方式包括:
- 自定义事件:使用
window.dispatchEvent和addEventListener实现发布-订阅模式,简单直接,但需要约定好事件名称和数据结构 - 共享状态:主应用维护一个全局状态容器(如 Redux Store、Vuex),子应用通过 props 或 API 获取/修改状态
- URL 参数:通过 URL hash 或 query string 传递简单数据,天然无耦合,适合用于路由级别的信息传递
- 消息总线:使用专门的消息总线库(如
mitt、EventEmitter),提供类型安全和更丰富的生命周期管理
在设计通信方案时,应遵循最小通信原则——应用间共享的数据越少越好。理想状态下,子应用应当是自治的,仅在必要时才进行跨应用通信。
二、qiankun
qiankun 是蚂蚁集团开源的一套基于 single-spa 的微前端实现框架,是目前国内使用最广泛的微前端方案之一。
2.1 single-spa 封装
qiankun 底层基于 single-spa,但对其进行了大量封装和增强。single-spa 本身只提供了应用注册与生命周期管理的基础能力,而 qiankun 在此基础上额外提供了:
- 开箱即用的 HTML 入口:子应用只需暴露一个 HTML 地址,qiankun 会自动解析并加载 JS/CSS 资源
- 沙箱隔离:内置 Proxy 沙箱,无需用户额外配置
- 样式隔离:支持 Shadow DOM 和实验性样式隔离机制
- 预加载:空闲时自动预加载子应用资源,提升切换体验
与直接使用 single-spa 相比,qiankun 的接入成本显著降低。使用 single-spa 需要手动处理子应用的入口文件格式、全局变量清理、样式隔离等问题,而 qiankun 将这些重担全部封装在框架内部。
2.2 沙箱隔离
qiankun 的沙箱隔离是其核心特性之一。它通过以下机制确保子应用之间的 JS 隔离:
Proxy 沙箱:为每个子应用创建一个独立的 Proxy 对象,拦截对 window 的读写操作。当子应用运行时,所有对全局变量的访问都被重定向到代理对象上;当子应用卸载时,沙箱环境被销毁,所有副作用被自动清除。qiankun 提供了三种沙箱模式:
Sandbox(默认):基于 Proxy 的快照沙箱,适用于不支持 Proxy 的低版本浏览器时降级为快照模式SnapshotSandbox:通过window快照记录变更,在激活/失活时切换环境,兼容性好但性能较差LegacySandbox:基于 Proxy 的沙箱,支持深度拦截
沙箱的引入解决了微前端中最令人头疼的全局污染问题——子应用 A 定义的全局变量不会泄漏到子应用 B 中,即使两个子应用加载了同名但不兼容的第三方库,也能互不干扰。
2.3 样式隔离
样式隔离是微前端的另一大难题。qiankun 提供了两种样式隔离策略:
Shadow DOM:通过将子应用挂载到 Shadow DOM 中实现 CSS 样式隔离。Shadow DOM 天然具备样式作用域,子应用的样式完全不影响主应用和其他子应用。但缺点是一些需要穿透 Shadow DOM 的 UI 组件(如弹窗、Tooltip)可能出现样式丢失。
实验性样式隔离:qiankun 会遍历子应用的样式表,为每个 CSS 规则添加前缀选择器(如 div[data-qiankun-app1] .btn),实现轻量级的作用域限定。这种方案不影响 DOM 结构,兼容性更好,但无法处理动态插入的样式。
实践中,通常建议采用 CSS Modules、CSS-in-JS 或命名空间约定作为样式隔离的基础手段,将 qiankun 的样式隔离作为兜底方案。
2.4 生命周期
qiankun 沿用了 single-spa 的生命周期约定,每个子应用需要暴露以下生命周期钩子:
export async function bootstrap() {
// 初始化,仅在子应用第一次加载时调用一次
console.log('子应用 bootstrapped');
}
export async function mount(props) {
// 挂载,每次进入子应用时调用
// props 中包含主应用传递的通信接口
render(props);
}
export async function unmount(props) {
// 卸载,每次离开子应用时调用
// 清理副作用:销毁实例、移除事件监听
ReactDOM.unmountComponentAtNode(props.container);
}
export async function update(props) {
// 更新(可选),当 props 发生变化时调用
}这些生命周期函数由 qiankun 在合适的时机自动调用,确保子应用的资源按需加载和释放。
2.5 应用间通信
qiankun 内置了一套基于 initGlobalState 的通信机制:
// 主应用
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, token: '' });
actions.onGlobalStateChange((state, prev) => {
console.log('状态变化:', state, prev);
});
actions.setGlobalState({ user: { name: '张三' } });
// 子应用(通过 props 获取)
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log('子应用收到状态:', state);
});
props.setGlobalState({ /* ... */ });
}2.6 预加载
qiankun 支持两种预加载策略:
prefetch: true:在第一个子应用挂载完成后,开始预加载其他所有已注册子应用的静态资源prefetch: ['app1', 'app2']:指定需要预加载的子应用列表
预加载通过浏览器的空闲时间(requestIdleCallback)进行,不会阻塞主应用的渲染。预加载的资源包括 HTML 入口、JS 文件和 CSS 文件,但不执行脚本代码。
三、wujie
wujie(无界)是由腾讯 IMWeb 团队开源的微前端框架,采用了基于 WebComponent 的容器方案。
3.1 WebComponent 方案
wujie 的核心思路是利用 WebComponent(主要是 customElements 和 Shadow DOM)作为子应用的运行容器。每个子应用被包裹在一个自定义 WebComponent 中,利用浏览器原生的 Shadow DOM 实现 JS 和 CSS 的天然隔离。
与 qiankun 的沙箱模拟方案不同,wujie 的隔离方案更贴近浏览器底层机制:
// wujie 创建子应用容器
const { bus } = setupApp({
name: 'react-app',
url: '//localhost:3000',
el: '#sub-app-container',
});每个子应用运行在独立的 WebComponent 下,其 document 和 window 都被代理到 Shadow DOM 的上下文中,从根本上杜绝了全局变量冲突。
3.2 无侵入式
wujie 最突出的优势是对子应用几乎零改造。子应用不需要感知自己处于微前端环境中,不需要修改构建配置,不需要暴露约定的生命周期函数。wujie 通过代理 document、location、history 等全局 API,使子应用的代码无需任何改动即可运行。
这与 qiankun 形成鲜明对比——qiankun 要求子应用修改入口文件、暴露生命周期钩子、处理相对路径资源等问题。对于老旧系统的微前端化改造,wujie 的零侵入优势尤为明显。
3.3 性能优势
wujie 在性能方面有几个关键优化:
- 实例缓存:子应用首次加载后保持运行实例,切换时无需重新创建,切换速度接近于零
- 预执行:在空闲时预加载并预执行子应用 JS,用户真正访问时已经渲染完毕
- WebComponent 原生隔离:利用浏览器原生能力实现隔离,省去了 qiankun 中 Proxy 沙箱的快照比对开销
在子应用切换场景下,wujie 的切换耗时通常为 qiankun 的 1/3 到 1/2。
3.4 与 qiankun 对比
| 对比项 | wujie | qiankun |
|---|---|---|
| 隔离方案 | WebComponent + Proxy | Proxy 沙箱 |
| 侵入性 | 零侵入,子应用无需修改 | 需暴露生命周期、修改入口 |
| 样式隔离 | Shadow DOM 天然隔离 | 实验性样式隔离 / Shadow DOM |
| 性能 | 实例缓存,切换≈0 开销 | 每次切换需重建 |
| 通信 | bus 事件总线 | initGlobalState |
| 预加载 | 预执行 JS | 预加载资源不执行 |
| 学习成本 | 较低 | 中等 |
| 浏览器兼容 | 需 Chrome 63+ | 兼容 IE11(快照模式) |
四、Module Federation
Module Federation(模块联邦)是 Webpack 5 引入的一项革命性特性,它从构建工具层面实现了微前端的能力。
4.1 Webpack 5 的模块联邦
Module Federation 的核心思想是:一个应用的构建产物可以在运行时被另一个应用使用。它通过 Webpack 插件在构建时生成一份「联邦模块清单」,运行时通过动态加载远程模块的方式实现跨应用代码共享。
与 qiankun 和 wujie 这类运行时框架不同,Module Federation 本质上是构建时的能力——它在打包阶段就确定了哪些模块需要暴露(expose),哪些模块需要引用(remote)。
4.2 运行时加载
Module Federation 实现了真正意义上的运行时加载。当一个应用配置了远程模块时,Webpack 不会在构建时把远程模块的代码打包进产物,而是在运行时通过 JSONP 动态拉取远程的 JS chunk。
基本配置示例:
// webpack.config.js(作为提供方)
new ModuleFederationPlugin({
name: 'user_app',
filename: 'remoteEntry.js',
exposes: {
'./UserList': './src/components/UserList',
'./UserDetail': './src/components/UserDetail',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});// webpack.config.js(作为消费方)
new ModuleFederationPlugin({
name: 'shell_app',
remotes: {
user_app: 'user_app@http://localhost:3001/remoteEntry.js',
},
shared: { react: { singleton: true }, 'react-dom': { singleton: true } },
});4.3 共享依赖
shared 配置是 Module Federation 最强大的特性之一。它允许多个独立构建的应用共享同一份第三方依赖的副本,避免重复加载。
当多个应用共享 react 时,Module Federation 会确保整个系统中只加载一份 React 代码。如果不同应用依赖的 React 版本不同,可以通过 requiredVersion 和 eager 等配置项控制版本匹配策略:
shared: {
react: {
singleton: true, // 全局单例
requiredVersion: '^17.0.0 || ^18.0.0', // 版本范围
eager: false, // 是否同步加载
},
}4.4 版本冲突
共享依赖带来的核心挑战是版本冲突。当多个应用的共享依赖版本不兼容时,需要仔细设计版本策略:
- 严格模式:要求所有消费方的共享依赖版本完全一致,不匹配则报错
- 宽松模式:允许一定范围的版本差异,以最先加载的版本为准
- 多实例模式:允许多个版本的共享依赖共存,但会增加包体积
实践中推荐的做法是:团队内统一共享依赖的版本,通过 Monorepo 或 eslint 规则强制一致性,将版本冲突消灭在开发阶段。
4.5 expose / remote
Module Federation 的核心理念围绕两个角色:
- expose(暴露方):决定哪些模块可以对外共享,通常是一个独立部署的应用或组件库
- remote(消费方):决定引用哪些远程模块,在代码中使用
import('user_app/UserList')语法加载
expose 和 remote 的关系是灵活可逆的——同一个应用可以同时扮演两者的角色。这种对称性使得 Module Federation 非常适合于构建复杂的微前端生态系统,其中各个应用既是提供者也是消费者。
五、三种方案对比
| 维度 | qiankun | wujie | Module Federation |
|---|---|---|---|
| 技术类型 | 运行时框架 | 运行时框架 | 构建工具能力 |
| 隔离机制 | Proxy 沙箱 + 快照 | WebComponent + Proxy | 无(依赖构建隔离) |
| 样式隔离 | 实验性 / Shadow DOM | Shadow DOM 原生 | 需自行处理 |
| 子应用侵入性 | 中等 | 低(零侵入) | 高(需 Webpack 5) |
| 框架无关 | 是 | 是 | 是 |
| 独立部署 | 支持 | 支持 | 支持 |
| 通信机制 | initGlobalState | bus 事件总线 | 无内置(需自行实现) |
| 性能 | 中等(重建开销) | 优秀(实例缓存) | 高(原生 Webpack) |
| 预加载 | 预加载资源 | 预执行 JS | 异步 chunk 按需加载 |
| 学习曲线 | 中等 | 低 | 高(需理解 Webpack) |
| 浏览器兼容 | IE11+ | Chrome 63+ | 现代浏览器 |
| 社区活跃度 | 高 | 中 | 高 |
| 最佳场景 | 存量系统微前端化 | 快速接入、零改造 | 新项目、组件级共享 |
六、微前端常见问题
6.1 样式冲突
样式冲突是微前端中最常见也最棘手的问题之一。当一个子应用的 antd 样式覆盖了另一个子应用的样式时,往往导致 UI 异常。
解决方案:
- CSS Modules / CSS-in-JS:在构建阶段为样式添加唯一 hash,从根本上避免冲突
- 命名空间约定:所有子应用采用统一的前缀(如
.app-user-、.app-order-) - Shadow DOM:利用浏览器原生隔离机制
@layer规则:使用 CSS Cascade Layers 控制样式的优先级层级
6.2 路由冲突
当多个子应用各自使用独立的路由器时,路由冲突是一个常见问题。不同子应用的 URL 规则可能重叠,或者主应用路由与子应用路由之间产生冲突。
解决方案:
- 统一路由前缀:为每个子应用分配唯一的路由前缀(如
/user/*、/order/*) - 主应用路由代理:主应用统一管理路由分发,子应用只使用相对路由
- Hash 路由:使用 hash 模式替代 history 模式,减少 URL 冲突的概率
- 防止路由冒泡:确保子应用的路由事件不会冒泡到主应用
6.3 公共依赖
公共依赖处理不当会导致包体积膨胀、版本冲突、疑似 Bug 等问题。例如,子应用 A 引入了 React 18,子应用 B 引入了 React 17,两个版本共存导致内存占用翻倍。
解决策略:
- External 配置:将公共依赖标记为 external,通过 CDN 统一加载
- Module Federation shared:使用 shared 配置实现依赖共享
- Monorepo 统一管理:在 Monorepo 中使用 pnpm workspace 统一各子应用的依赖版本
- 构建时注入:主应用在运行时注入公共依赖,子应用在构建时排除这些依赖
6.4 开发调试
微前端的开发调试复杂度远高于单体应用,主要体现在:子应用需要独立开发但需要在主应用上下文中验证;跨应用调用的问题复现困难;子应用之间的交互联调流程繁琐。
最佳实践:
- 子应用独立运行:每个子应用应支持独立启动和运行,开发模式不受主应用影响
- Mock 数据层:子应用在独立开发时使用 mock API,接入主应用时切换为真实 API
- Chrome 扩展工具:使用 qiankun DevTools 等专用调试工具
- 统一日志系统:所有子应用接入统一的日志采集系统,便于排查问题
- CI/CD 集成:使用 Storybook 或类似的组件预览工具,在 CI 中自动生成子应用的独立预览页面
- E2E 测试:使用 Playwright 或 Cypress 编写跨应用的端到端测试用例,覆盖完整的用户操作链路
微前端并非银弹,在引入时需要充分评估团队的技术储备和业务需求。对于小型团队或简单应用,单体架构仍然是更高效的选择。当应用规模和团队复杂度确实达到微前端的门槛时,qiankun 的成熟稳定、wujie 的低侵入性、Module Federation 的构建原生能力,分别适合不同的场景。建议从最小可行方案开始,渐进式地引入微前端能力,避免过度设计。