引言
微前端不是一个新的框架,而是一套把单体前端拆分为多个可独立开发、独立部署、独立运行的子应用的架构模式。它解决的核心痛点是大型团队的协作边界与发布耦合。本文从动机出发,拆解 qiankun(single-spa)的加载与沙箱机制、webpack 5 Module Federation 的运行时共享原理、样式/路由/状态隔离方案,并对两种主流路线的取舍给出工程化建议。文中涉及的 qiankun、single-spa、Module Federation 均为真实存在的生产级方案。
一、微前端的动机与收益
1.1 单体前端的组织困境
当产品演进到一定规模,单体前端会暴露三类问题:
- 发布耦合:任何一个模块的改动都需要整站回归、整体发布,小步快跑变成奢望。
- 技术栈锁定:老模块用 AngularJS,新团队想上 Vue 3,但在单体内无法共存,只能重写或继续妥协。
- 团队边界模糊:多团队在同一代码库内频繁冲突,代码所有权难以界定。
1.2 微前端的核心收益
微前端将整个应用拆成主应用(基座)+ 若干子应用。每个子应用可以拥有独立的仓库、技术栈、发布节奏和团队。主应用负责统一壳层(Shell):顶部导航、登录态、全局样式、路由分发。
| 收益 | 说明 | 代价 |
|---|---|---|
| 独立部署 | 子应用可单独发版、回滚 | 版本兼容矩阵管理 |
| 技术栈解耦 | 各子应用可自由选型 | 共享能力需要标准接口 |
| 团队自治 | 代码所有权清晰 | 基建与规范仍需统一 |
| 渐进迁移 | 老系统可逐步被替换 | 双轨期通信与体验一致性 |
微前端的本质是组织架构问题的技术映射:它把"一个大型应用"重新组织为"一个平台 + 多个产品"。
二、qiankun 机制:single-spa 之上的开箱即用
2.1 qiankun 与 single-spa 的关系
qiankun 是基于 single-spa 的上层封装。single-spa 负责最核心的应用注册与生命周期调度,但它要求开发者自行处理 HTML 入口解析、JS 沙箱、样式隔离,这些恰恰是最容易出错的环节。qiankun 把这些能力内置,提供开箱即用的接入体验。
主应用通过 registerMicroApps 注册子应用,配置其入口 HTML 地址与路由激活规则:
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react-app',
entry: '//localhost:7101',
container: '#micro-app-container',
activeRule: '/app/react',
},
{
name: 'vue-app',
entry: '//localhost:7102',
container: '#micro-app-container',
activeRule: '/app/vue',
},
]);
start();
2.2 子应用生命周期
qiankun 要求子应用暴露三个生命周期函数,并在入口文件中通过 qiankun 的全局接口导出:
// 子应用入口 main.js
import { bootstrap, mount, unmount } from './app';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun';
renderWithQiankun({
bootstrap() {},
async mount(props) {
// props 中包含主应用传入的通信数据、路由等
render(props);
},
async unmount() {
// 卸载:清理事件监听、销毁实例
},
});
// 非 qiankun 环境(独立运行时)也需要可用的导出
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({});
}
mount 与 unmount 是沙箱生命周期中最重要的两个钩子——前者完成子应用挂载,后者必须干净地清理副作用,否则子应用切换后会产生事件泄漏与内存残留。
2.3 加载机制:从 HTML Entry 到资源解析
qiankun 支持两种入口方式:JS Entry 与 HTML Entry。实践中推荐 HTML Entry——qiankun 会 fetch 入口 HTML,解析其中的 <script> 与 <link>,把脚本逐一执行到主应用的沙箱环境中,样式标签也一并接管。这样子应用无需感知 qiankun 存在,理论上可以零改造接入。
| 入口方式 | 优点 | 缺点 |
|---|---|---|
| JS Entry | 产物确定、静态分析容易 | 子应用必须按约定改造 |
| HTML Entry | 子应用可零改造 | 需要处理外链资源、跨域 fetch |
三、Module Federation:webpack 5 的运行时模块共享
3.1 从构建期共享到运行时共享
webpack 5 引入 Module Federation(MF),让多个独立构建的应用可以在运行时互相加载彼此的模块,而无需把依赖打包进每个应用。与 qiankun"加载整个子应用"不同,MF 的粒度更细——它可以只共享一个组件、一个工具库。
宿主应用(Host)在 webpack 配置中声明需要哪些远程模块:
// webpack.config.js(宿主应用)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
remoteApp: 'remoteApp@http://localhost:7201/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
}),
],
};
远程应用(Remote)暴露自己的组件:
// webpack.config.js(远程应用)
new ModuleFederationPlugin({
name: 'remoteApp',
exposes: {
'./ProductList': './src/components/ProductList.jsx',
'./PriceBadge': './src/components/PriceBadge.jsx',
},
shared: { react: { singleton: true } },
});
3.2 运行时加载远程组件
宿主应用通过动态 import 引用远程模块,webpack 在运行时解析 remoteEntry.js:
import { Suspense, lazy } from 'react';
const ProductList = lazy(() => import('remoteApp/ProductList'));
function App() {
return (
<Suspense fallback={<div>加载远程组件…</div>}>
<ProductList />
</Suspense>
);
}
3.3 shared 与 singleton
shared 配置用于共享依赖,singleton: true 表示全应用只保留一份 React 实例。这能避免"两份 React 导致 hooks 失效"的经典问题——两个 React 实例并存时,useState 等 hooks 会因内部 dispatcher 冲突而报错。requiredVersion 则在共享失败时提供降级提示。
| 配置项 | 作用 | 常见坑 |
|---|---|---|
| singleton | 全局唯一实例 | 版本不一致时仍会各自加载 |
| requiredVersion | 声明版本约束 | 约束过严导致无法共享 |
| shareScope | 共享作用域 | 多 scope 时需显式指定 |
四、样式隔离与路由隔离
4.1 样式隔离方案
子应用之间的 CSS 互相污染是微前端最隐蔽的坑。qiankun 默认提供三种层级:主应用全局样式、子应用在激活时注入的样式、运行时动态样式。常见的隔离手段包括:
- 前缀命名空间(BEM/约定):所有子应用类名强制加前缀,如
.react-app-product-。最可靠但依赖规范。 - CSS Modules / scoped CSS:编译期给类名加 hash,天然隔离。
- Shadow DOM:qiankun 支持
sandbox: { strictStyleIsolation: true }把子应用包裹进 Shadow DOM,样式完全隔离,但需处理弹出层(弹窗挂载在 body)穿透的问题。
/* 子应用内的 scoped 样式,Vue 单文件组件示例 */
<style scoped>
.product-card {
border: 1px solid var(--color-border);
border-radius: 8px;
}
</style>
4.2 路由隔离与主子应用路由协调
每个子应用必须明确自己的路由前缀,主应用依据 activeRule 判断何时激活哪个子应用。React Router 子应用需要基于前缀创建 basename:
// 子应用 React Router 配置
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const { qiankunWindow } = require('vite-plugin-qiankun');
const base = qiankunWindow.__POWERED_BY_QIANKUN__ ? '/app/react' : '/';
export function Root() {
return (
<BrowserRouter basename={base}>
<Routes>
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
</Routes>
</BrowserRouter>
);
}
路由隔离的要点:子应用内部跳转使用相对路径或基于 basename;跨应用跳转通过主应用路由或通信机制,避免子应用直接操作 window.location 破坏激活判定。
五、状态隔离与沙箱
5.1 JavaScript 沙箱
qiankun 的沙箱通过在 window 上做代理 + 快照实现。激活子应用时,把子应用对 window 的读写代理到一个独立对象上;子应用卸载后,把修改过的属性快照存起来,恢复主应用环境,下次激活时再恢复快照。这样即使子应用代码直接修改全局变量,也不会污染主应用或其他子应用。
qiankun 提供 sandbox: true(默认)与更严格的 strictStyleIsolation。从 v2.x 起支持 legacySandbox 与新的基于 Proxy 的实现,后者性能更好。对不支持 Proxy 的旧浏览器可退化为快照式沙箱。
沙箱保护的是主应用环境的完整,而不是子应用之间的数据隔离。跨子应用共享登录态与用户信息仍需要显式的通信契约,不要指望沙箱替你解决状态共享。
5.2 状态隔离的建议
需要澄清的是:沙箱隔离的是全局环境,而不是状态共享。微前端架构里,子应用之间、子应用与主应用之间仍然需要共享登录态、用户信息等。状态隔离的正确姿势是"共享最小化 + 接口标准化":
- 登录态、用户信息由主应用统一持有,通过初始化 props 或全局事件广播给子应用。
- 子应用内部的业务状态彼此独立,不共享全局 Store。
- 通信数据尽量通过主应用转发,避免子应用直接耦合。
// 主应用向子应用注入 props
registerMicroApps([
{
name: 'react-app',
entry: '//localhost:7101',
container: '#app',
activeRule: '/app/react',
props: {
userInfo: { id: 1, name: 'Leeting' },
token: 'xxx',
onNavigate: (path) => history.push(path),
},
},
]);
六、公共依赖与共享策略
6.1 共享的粒度决策
公共依赖共享需要回答三个问题:共享什么(react、UI 库、工具函数)、以什么方式共享(构建期 externals、运行时 MF、CDN)、谁来治理版本(主应用统一、子应用自治)。qiankun 生态常用 externals 把公共依赖排除出子应用产物,运行时从主应用全局变量取用:
// 子应用 vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});
6.2 双 React 问题的根因
“子应用用了自己的 React,主应用也有一份 React"会造成 hooks 报错、事件系统重复。根治手段:
- 统一运行时:所有子应用通过 externals/MF 共享主应用提供的同一份 React。
- 或者各子应用完全自包含:不共享 React,但代价是包体积翻倍、内存多一份实例。
- 折中:共享 react/react-dom,UI 组件库如 antd 保持 singleton。
| 共享方式 | 复杂度 | 版本一致性 | 包体积 |
|---|---|---|---|
| externals(qiankun) | 低 | 主应用统一 | 最优 |
| Module Federation shared | 中 | 运行时协商 | 次优 |
| 完全自包含 | 无 | 各自独立 | 冗余 |
七、通信方案:从事件总线到消息契约
7.1 常用通信机制
微前端的通信是"既要又要"的难题:既要松耦合,又要有可靠的消息契约。主流机制有三类:
- props 透传(qiankun 初始化注入):适合一次性配置数据(登录态、基础配置)。
- 全局事件总线(mitt 等):适合低频广播,但事件名无类型约束,容易失控。
- 共享 Store / URL:适合跨应用同步的筛选条件、导航状态。
// 用 mitt 实现跨子应用事件广播(主应用持有)
import mitt from 'mitt';
export const globalBus = mitt();
// 子应用 A 发布
globalBus.emit('user:logout', { reason: 'session-expired' });
// 子应用 B 订阅
globalBus.on('user:logout', ({ reason }) => {
// 清理本应用内的会话缓存
console.log(`登出原因:${reason}`);
});
7.2 通信的最佳实践
实践中最可靠的不是"任意事件随意广播”,而是建立消息契约层——主应用维护一张消息清单,定义事件名、载荷类型、方向(主→子、子→主、子→子)。可以把契约写进一个共享的 npm 包,或通过主应用 props 注入一份只读的契约对象。对 TypeScript 团队,建议用类型化的事件映射定义:
type GlobalEvents = {
'user:logout': { reason: string; at: number };
'route:change': { path: string };
'theme:update': { theme: 'light' | 'dark' };
};
这样广播与订阅都能获得编译期类型检查,大幅降低通信事故率。
八、部署与版本治理
8.1 独立部署的产物形态
子应用独立部署意味着每个子应用有自己的域名路径或 CDN 前缀。qiankun 场景下主应用通过 entry 指向子应用的 HTML;MF 场景下宿主应用通过 remoteEntry.js 指向远程模块清单。关键约束是部署顺序:主应用不应在子应用未上线时引用不存在的地址,否则整站白屏。
推荐的产物布局:
# CDN 上的目录结构
/apps/
main/ # 主应用
index.html
react-app/ # 子应用 A
index.html
assets/
vue-app/ # 子应用 B
index.html
assets/
8.2 版本兼容矩阵
子应用独立发布后,主应用与各子应用之间形成隐式契约:主应用传给子应用的 props 结构、子应用暴露的公共模块接口,都必须向前兼容。治理手段:
- 对外接口变化遵循语义化版本,破坏性变更前发布兼容期。
- 主应用建立"已支持子应用版本"清单,激活前做版本校验。
- 灰度发布:先让部分流量命中新版本子应用,观察错误率再全量。
| 治理工具 | 解决什么 | 局限 |
|---|---|---|
| 接口版本号 | 契约兼容性 | 需要子应用配合 |
| 灰度发布 | 降低变更风险 | 需要流量基础设施 |
| 主应用版本清单 | 明确支持范围 | 手动维护成本高 |
九、qiankun vs Module Federation 对比与常见坑
9.1 两大路线对比矩阵
| 维度 | qiankun(single-spa) | Module Federation |
|---|---|---|
| 抽象层级 | 应用级 | 模块级 |
| 核心机制 | 应用注册 + 生命周期 + 沙箱 | 运行时远程模块加载 |
| 隔离能力 | JS 沙箱 + 样式隔离内置 | 需自行实现沙箱 |
| 技术栈约束 | 几乎不约束 | 绑定 webpack 5(Rspack 兼容) |
| 接入改造 | 子应用需暴露生命周期 | 构建配置声明 |
| 典型场景 | 老系统渐进改造、团队自治 | 微组件共享、依赖复用 |
| 社区成熟度 | 国内生态成熟 | 海外生态活跃 |
9.2 高频踩坑清单
- 双 React 实例:hooks 报错
Invalid hook call。排查方式:检查子应用产物里是否内联了 react。 - 沙箱下定时器/事件泄漏:
unmount中未清除setInterval与 DOM 事件监听,导致切换应用后主应用卡顿。 - 样式互相污染:全局 reset 被子应用覆盖。应对:命名前缀 + scoped + 必要时 Shadow DOM。
- 路由激活错误:子应用 hash 路由与主应用 history 路由混用导致不激活。统一路由模式是关键。
- MF 远程地址写死:本地指向
localhost的remoteEntry.js部署后失效。应使用环境变量注入地址。 - 依赖共享版本冲突:
requiredVersion配置过严导致运行时告警。应放宽为^范围或完全 singleton。
9.3 何时不要用微前端
微前端并非银弹。团队小于 20 人、应用规模中等、技术栈统一的情况下,Monorepo + 单体应用往往更简单高效。微前端的收益来自"组织规模"而非"技术规模"——没有真正的多团队独立发布诉求时,引入微前端只会徒增沙箱、通信、部署三层复杂度。
一个可靠的判断信号:如果你说不出「哪些团队需要独立发布」的具体清单,那你很可能不需要微前端。
# 一个典型的主应用启动命令(开发环境)
npm run dev:main
# 子应用 A 单独启动
cd apps/react-app && npm run dev:7101
结语
微前端是组织架构问题的技术回应。qiankun 通过 single-spa 的应用生命周期 + 沙箱体系,让多团队应用共存成为可能;Module Federation 则把共享粒度下沉到模块级,适合依赖复用与渐进整合。两者的共同前提是:明确主应用与子应用的边界、建立消息契约、治理版本兼容矩阵。
如果你正处在要不要上微前端的决策点,我的建议是:先回答"是否有多个团队需要独立部署",再回答"存量系统是否需要渐进迁移"。两个答案都是"是"时才值得投入;否则,一个治理良好的 Monorepo 单体应用,往往比一套勉强运转的微前端系统更可靠。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。