微前端 — Micro Frontend
1. 核心概念与动机
定义
微前端是将前端应用拆分为多个独立子应用,每个子应用可独立开发、测试、部署,运行时通过容器动态聚合为完整应用。核心原则:技术栈无关、独立仓库、独立生命周期、自治团队。
解决的问题
| 问题 |
具体表现 |
| 巨石应用难维护 |
代码量数十万行、多团队 Git 冲突频繁、构建超 5 分钟 |
| 技术栈锁定 |
老项目无法渐进升级(jQuery → Vue → React),新功能被迫沿用旧框架 |
| 跨团队协作瓶颈 |
发布排期耦合、feature flag 膨胀、Code Review 跨域困难 |
| 独立交付受阻 |
一个小组的 bug 阻塞全应用发布、回滚范围过大 |
适用场景
- 后台管理系统:多业务线聚合(CMS/订单/用户/支付各自独立团队)
- 大型 C 端应用:多团队并行迭代(首页/搜索/详情/个人中心独立发布)
- 渐进式技术改造:新功能用新框架(React 18 / Vue 3),老系统保持不动
- B2B 可组合平台:不同客户选择不同模块组合(如电商平台功能开关)
2. 主流方案详解
2.1 qiankun(阿里,基于 single-spa)
架构原理
主应用 ──注册→ 子应用列表(activeRule ↔ name ↔ entry)
│
├─ 路由变化匹配 activeRule
├─ HTML Entry 加载子应用 HTML → 解析出 JS/CSS
├─ 创建 JS 沙箱 + CSS 隔离
├─ 执行生命周期: bootstrap → mount → unmount → update
└─ 渲染到指定 DOM 容器 (#app)
主应用注册代码
// apps/main/src/micro/index.ts
import { registerMicroApps, start, initGlobalState } from 'qiankun';
import type { ObjectType } from '@qiankunjs/core';
interface AppConfig {
name: string;
entry: string; // 子应用 HTML 地址
container: string; // 挂载 DOM 选择器
activeRule: string; // 路由前缀匹配
props?: ObjectType; // 传递给子应用的数据
}
const apps: AppConfig[] = [
{
name: 'app-order',
entry: '//localhost:3001',
container: '#sub-app-container',
activeRule: '/order',
props: { baseUrl: '/order' },
},
{
name: 'app-user',
entry: '//localhost:3002',
container: '#sub-app-container',
activeRule: '/user',
},
];
registerMicroApps(apps, {
beforeLoad: [async (app) => console.log('[qiankun] before load', app.name)],
beforeMount: [async (app) => console.log('[qiankun] before mount', app.name)],
afterMount: [async (app) => console.log('[qiankun] after mount', app.name)],
beforeUnmount: [async (app) => console.log('[qiankun] before unmount', app.name)],
afterUnmount: [async (app) => console.log('[qiankun] after unmount', app.name)],
});
// 启动(可配置 prefetch / sandbox / singular 等)
start({ sandbox: { experimentalStyleIsolation: true }, prefetch: 'all' });
子应用入口(以 Umi 为例)
// apps/order/src/app.ts
export const qiankun = {
async bootstrap() {
console.log('order app bootstraped');
},
async mount(props: any) {
render(props); // props 含 container/onGlobalStateChange/setGlobalState
},
async unmount(props: any) {
ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root')! : document.getElementById('root'));
},
};
生命周期
beforeLoad → bootstrap → beforeMount → mount → afterMount
↓ (路由失活)
beforeUnmount → unmount → afterUnmount
CSS 隔离
| 模式 |
实现方式 |
优缺点 |
experimentalStyleIsolation |
每个子应用包裹 Shadow DOM |
兼容性问题(弹窗/Dialog 等 appendToBody 会逃逸) |
strictStyleIsolation |
Scoped CSS(给选择器加 div[data-qiankun-appName] 前缀) |
不防动态插入的 style 标签,性能损耗 |
| 自定义方案 |
postcss-prefix-selector + BEM |
需要构建配合,兼容性好 |
JS 沙箱
| 沙箱类型 |
原理 |
兼容性 |
性能 |
| ProxySandbox |
ES6 Proxy 代理 window 对象,激活/失活时切换虚拟 window 与真实 window |
IE11 不支持 |
高 |
| SnapshotSandbox |
激活时保存 window 快照 → 失活时遍历恢复差异 |
全兼容 |
低(大量对象遍历) |
| LegacySandbox |
单实例 Proxy 沙箱(仅全局 window 代理) |
同上 Proxy |
中等 |
// SnapshotSandbox 简版示意
class SnapshotSandbox {
private snapshot: Record<string, any> = {};
private modifiedProps: Record<string, any> = {};
activate() {
this.snapshot = {};
for (const key in window) {
this.snapshot[key] = window[key]; // 保存快照
}
Object.assign(window, this.modifiedProps); // 恢复上次修改
}
deactivate() {
for (const key in window) {
if (window[key] !== this.snapshot[key]) {
this.modifiedProps[key] = window[key]; // 记录差异
window[key] = this.snapshot[key]; // 恢复快照
}
}
}
}
通信机制:initGlobalState
// 主应用
const { onGlobalStateChange, setGlobalState } = initGlobalState({ user: null, token: '' });
onGlobalStateChange((state, prev) => {
console.log('[qiankun] global state changed', state, prev);
});
setGlobalState({ user: { id: 1, name: 'admin' }, token: 'xxx' });
// 子应用
props.onGlobalStateChange((state, prev) => { /* 同步 */ }, true); // true = 立即触发
props.setGlobalState({ /* 局部更新 */ });
预加载 prefetch
start({
prefetch: 'all', // 所有子应用
prefetch: ['app-order'], // 指定列表
prefetch: true, // 第一个子应用加载完后预加载其余
});
常见坑
| 问题 |
解决方案 |
| 子应用路由前缀 |
activeRule: '/order' + 子应用 router base: /order |
| 静态资源 404 |
__webpack_public_path__ 动态设置:window.__POWERED_BY_QIANKUN__ && __webpack_public_path__ |
| antd/Element UI 样式冲突 |
开启 experimentalStyleIsolation 或主应用/子应用各用不同版本且 CSS 加前缀 |
| 子应用弹窗位置错乱 |
Dialog/Modal 的 getContainer 指定为子应用容器节点 |
| 全局事件污染 |
unmount 时清理 addEventListener / setInterval |
2.2 Module Federation(Webpack 5 / Rspack)
核心概念
| 角色 |
说明 |
| Host |
消费者,运行时加载远程模块 |
| Remote |
生产者,暴露模块供 Host 使用 |
| exposes |
Remote 声明哪些模块对外暴露 |
| remotes |
Host 声明从哪个 Remote 加载模块 |
| shared |
共享依赖(React/Vue/Lodash),避免重复加载 |
| singleton |
强制单例(防止多 React 实例导致 hooks 报错) |
| remoteEntry.js |
Remote 的加载入口文件,包含模块映射清单 |
配置详解
// apps/remote/webpack.config.js —— Remote 生产者
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: { publicPath: 'http://localhost:3001/' },
plugins: [
new ModuleFederationPlugin({
name: 'order',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/pages/OrderList',
'./OrderDetail': './src/pages/OrderDetail',
'./store': './src/store',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
'react-router-dom': { singleton: true },
},
}),
],
};
// apps/host/webpack.config.js —— Host 消费者
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
order: 'order@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// apps/host/src/App.tsx —— 使用 Remote 模块
const OrderList = React.lazy(() => import('order/OrderList'));
function App() {
return (
<React.Suspense fallback={<Spin />}>
<OrderList />
</React.Suspense>
);
}
加载原理
Host 运行时执行:
import('order/OrderList')
→ 动态创建 <script src="http://localhost:3001/remoteEntry.js">
→ remoteEntry.js 自执行,在 __webpack_modules__ 注册所有 exposes 模块
→ 检查 shared 作用域: react/react-dom 版本是否满足 → 不满足则加载 Remote 的副本
→ 返回 OrderList 模块 → Host 渲染
shared 关键属性
shared: {
react: {
singleton: true, // 强制全局只有一个 React 实例
requiredVersion: '^18.0.0', // 版本约束,不满足时 console.warn
eager: true, // 立即加载(非懒加载,首屏可用)
strictVersion: true, // 版本不匹配时直接抛错
shareScope: 'default', // 共享作用域名称,多组隔离用
},
}
Module Federation vs qiankun 本质区别
| 维度 |
Module Federation |
qiankun |
| 组合粒度 |
模块级(组件/工具/Hooks) |
应用级(完整子应用,路由维度) |
| 仓库关系 |
多个模块可同仓库,也可跨仓库 |
每个子应用独立仓库 |
| 构建紧密度 |
构建时需知道 remote 地址 |
运行时通过 HTML Entry 发现 |
| 技术栈绑定 |
共享 same JS 运行时(必须 webpack 5+) |
不限(HTML Entry 加载裸 HTML) |
| 部署关联 |
Remote 更新后 Host 需重新构建共享?实际运行时加载最新 remoteEntry |
子应用独立部署,主应用自动获取最新 |
| 沙箱隔离 |
无(靠 shared singleton 约定) |
Proxy/快照沙箱 + CSS 隔离 |
| 适用场景 |
微模块/组件共享/Monorepo 补充 |
微前端应用组合 |
Rspack Module Federation
Rspack 的 MF 实现与 Webpack MF 配置完全兼容,仅需替换插件包名:
// rspack.config.js —— 与 webpack 配置几乎一致
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack');
// 或 rspack 内置: rspack.ModuleFederationPlugin
优势:构建速度提升 5-10x(Rust 编写)、HMR 更快、配置对齐 Webpack 生态。
常见坑
| 问题 |
解决方案 |
| React 多实例(hooks 报错) |
shared: { react: { singleton: true } } 必须两端配置一致 |
| Remote 加载失败 |
React.lazy + ErrorBoundary 包裹: |
| CSS 重复加载 |
shared 无法共享 CSS;需写 CSS 约定(BEM/CSS Modules)或用 tailwind |
| shared 版本不匹配静默 |
加 strictVersion: true 在构建时报错 |
// 降级处理
function OrderPage() {
return (
<ErrorBoundary fallback={<Alert message="订单模块加载失败,请刷新重试" type="error" />}>
<React.Suspense fallback={<Skeleton active />}>
<OrderList />
</React.Suspense>
</ErrorBoundary>
);
}
2.3 wujie(腾讯)
核心思路
iframe + Web Component 混合方案。子应用运行在 iframe 中(天然硬隔离),通过 Web Component 自定义元素挂载到主应用 DOM 树。
主应用 DOM
└─ <wujie-app name="order" url="http://localhost:3001">
└─ #shadow-root (Web Component)
└─ <iframe srcdoc="...">
└─ 子应用完整运行环境(window/document/location 全部隔离)
使用方式
// 主应用
import { Wujie } from 'wujie-react';
function App() {
const [show, setShow] = useState(true);
return (
<Wujie
name="order"
url="//localhost:3001"
alive={true} // 保活模式(keep-alive,切回时不重新加载)
props={{ user: { id: 1 } }} // 传递数据
sync={true} // 同步路由
fetch={customFetch} // 自定义 fetch(用于鉴权 header 注入)
beforeLoad={/* ... */}
beforeMount={/* ... */}
/>
);
}
硬隔离 vs 软隔离
| 维度 |
wujie(硬隔离) |
qiankun(软隔离) |
| JS 隔离 |
iframe 天然隔离,window/document/location 完全独立 |
Proxy 沙箱模拟隔离,仍有逃逸风险(如 Object.defineProperty) |
| CSS 隔离 |
iframe 天然隔离 |
Scoped CSS / Shadow DOM,弹窗/Dialog 易逃逸 |
| Cookie/LocalStorage |
完全隔离(可通过 postMessage 同步) |
共享主应用(需手动隔离 key 命名空间) |
| 通信开销 |
postMessage(异步,序列化) |
全局状态同步(同步,引用传递) |
| 子应用改造量 |
几乎为零(无需改源码) |
需要入口文件暴露生命周期 |
| 安全风险 |
低(iframe 沙箱 + sandbox 属性) |
中(Proxy 沙箱有已知逃逸漏洞) |
通信
// props 传参
<Wujie name="order" url="..." props={{ token: 'xxx' }} />
// 主 → 子
wujieBus.$emit('order:refresh', { id: 1 });
// 子 → 主(子应用内)
window.$wujie?.bus.$emit('global:logout');
// 或
window.$wujie?.props.methods.someCallback(data);
2.4 micro-app(京东)
核心思路
类 Web Component 思想,使用 CustomElement + Shadow DOM 接入子应用。接入成本极低,类似 iframe 的无缝替换体验。
<!-- 主应用直接使用自定义标签加载子应用 -->
<micro-app
name="order"
url="http://localhost:3001"
baseroute="/order"
disabledScopecss={false}
iframe={false}
></micro-app>
子应用
// 子应用入口处(如 main.js)
if (window.__MICRO_APP_ENVIRONMENT__) {
// 微前端环境下设置 publicPath 和路由 base
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__;
}
生命周期
| 事件 |
说明 |
created |
元素创建 |
beforemount |
开始加载 |
mounted |
加载完成并渲染 |
unmount |
卸载 |
error |
加载失败 |
document.querySelector('micro-app[name="order"]')?.addEventListener('mounted', (e) => {
console.log('order app mounted', e);
});
通信
// 主 → 子
microApp.setData('order', { type: 'newMsg', data: { hello: 'world' } });
// 子 → 主
window.microApp?.dispatch({ type: 'logout' });
特点
- 接入成本最低:一行自定义标签即可加载子应用
- Shadow DOM 可选:开启后 CSS 隔离效果好,但兼容 antd 等 appendToBody 弹窗需配置
- iframe 降级:
iframe={true} 可回退到 iframe 模式
2.5 四方案对比表
| 维度 |
qiankun |
Module Federation |
wujie |
micro-app |
| 隔离粒度 |
应用级 |
模块级(组件/库) |
应用级(硬隔离) |
应用级 |
| JS 沙箱 |
Proxy / 快照 |
共享作用域(无隔离) |
iframe 硬隔离 |
Proxy |
| CSS 隔离 |
实验性 / Scoped 前缀 |
无原生(需约定) |
iframe 硬隔离 |
Scoped CSS / Shadow DOM |
| 通信机制 |
initGlobalState |
共享模块 / shared |
postMessage / EventBus |
EventBus |
| 子应用改造量 |
需暴露生命周期 |
零(组件级引用) |
几乎为零 |
极少(仅 publicPath) |
| 技术栈限制 |
不限(HTML Entry) |
限同一构建体系(Webpack/Rspack) |
不限(iframe) |
不限 |
| 首屏性能 |
中(HTML Entry 解析开销) |
优(模块级懒加载) |
中(iframe 初始化开销) |
优 |
| 兼容性 |
IE11(SnapshotSandbox) |
现代浏览器 |
现代浏览器 |
现代浏览器 |
| 学习成本 |
中 |
高(理解共享作用域概念) |
低 |
低 |
| 社区活跃度 |
高(阿里,star 16k+) |
极高(webpack 官方) |
中(腾讯) |
中(京东) |
| 适用场景 |
后台系统 / 多团队并行 |
组件级共享 / 微模块架构 |
安全隔离要求高 / 老旧系统嵌入 |
快速接入 / 迁移 |
3. 关键实战问题
3.1 公共依赖处理
| 方案 |
原理 |
体积 |
隔离性 |
适用场景 |
| externals(CDN) |
React/Vue/Lodash 通过 CDN 加载,全局共享 |
极小(单份) |
弱(window.React 全局) |
技术栈统一、部署简单 |
| shared(MF) |
webpack shared scope 复用已加载模块 |
小(按版本分 chunk) |
中(singleton 约束版本) |
多应用 MF 共享 |
| 独立打包 |
每个子应用各自打包自己的依赖 |
大(N 份) |
强(完全隔离) |
技术栈不同、必须隔离 |
| qiankun externals |
主应用挂载全局 + 子应用不打入公共库 |
中 |
中(版本需一致) |
qiankun 体系 |
经验法则:
- 同框架同版本 → externals 或 shared(省体积)
- 同框架不同版本 → shared singleton 限制版本范围,或各自打包(省心但费体积)
- 不同框架 → 各自打包,无共享
3.2 样式隔离方案
| 方案 |
原理 |
兼容性 |
维护成本 |
| BEM 命名约定 |
.order-list__item--active 加命名空间前缀 |
全兼容 |
高(需团队约定+review) |
| CSS Modules |
.module.css 编译生成哈希类名 |
全兼容 |
中(需构建支持) |
| CSS-in-JS |
styled-components / emotion 运行时隔离 |
全兼容 |
中(运行时开销) |
| Shadow DOM |
浏览器原生样式隔离 |
部分(antd 弹窗逃逸) |
低(框架自带) |
| postcss-prefix-selector |
自动给所有选择器加前缀 .app-order |
全兼容 |
低(构建自动处理) |
// postcss.config.js —— 自动加前缀
module.exports = {
plugins: [
require('postcss-prefix-selector')({
prefix: '.app-order',
transform(prefix, selector, prefixedSelector) {
// 保留 :root / :global 开头的选择器
if (selector.startsWith(':root') || selector.startsWith(':global')) {
return selector;
}
return prefixedSelector;
},
}),
],
};
3.3 登录态与鉴权
主应用
├─ 统一登录页面 → 获取 JWT Token
├─ Token 存入 localStorage(约定 key: AUTH_TOKEN)
├─ 子应用加载时通过 props / postMessage 透传
└─ Token 刷新:主应用定时刷新后同步给所有子应用
问题点:
├─ Cookie SameSite 策略(Chrome 默认 Lax 会拦截 iframe 内 Cookie)
├─ SSO 单点登录(子应用各自跳转 SSO 时需统一回跳地址)
└─ Token 过期同步(子应用检测 401 → 通知主应用刷新 → 重试请求)
// 主应用统一 token 刷新逻辑
let isRefreshing = false;
let failedQueue: Array<{resolve: Function; reject: Function}> = [];
function refreshToken(): Promise<string> {
return axios.post('/api/auth/refresh', { refreshToken: storage.get('REFRESH_TOKEN') });
}
// axios 响应拦截器
axios.interceptors.response.use(
(res) => res,
async (error) => {
if (error.response?.status !== 401) return Promise.reject(error);
if (!isRefreshing) {
isRefreshing = true;
try {
const { token } = await refreshToken();
storage.set('AUTH_TOKEN', token);
// 同步给所有子应用
microApps.forEach((app) => app.setGlobalState?.({ token }));
isRefreshing = false;
failedQueue.forEach(({ resolve }) => resolve(token));
failedQueue = [];
return axios(error.config);
} catch {
isRefreshing = false;
failedQueue.forEach(({ reject }) => reject(error));
failedQueue = [];
// 跳转登录页
}
} else {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
});
}
},
);
3.4 子应用缓存策略
| 策略 |
做法 |
效果 |
| 版本文件名 |
构建产出含 hash(main.a1b2c3.js) |
用户始终获取最新版本 |
| HTML 缓存时间 |
HTML 设置 Cache-Control: no-cache |
浏览器每次请求最新 HTML 入口 |
| CDN 缓存 |
静态资源(JS/CSS/IMG)设置长期 max-age=31536000 + hash |
缓存命中率高,版本切换自动失效 |
| 灰度发布 |
根据 userId/cookie 分流不同版本 |
小范围验证后全量 |
| 特性开关 |
通过 props 传递 featureFlag |
动态控制功能显隐,无需重新部署 |
# HTML 入口不缓存
location /order {
add_header Cache-Control "no-cache, must-revalidate";
try_files $uri /order/index.html;
}
# 静态资源长期缓存
location /order/static {
add_header Cache-Control "public, max-age=31536000, immutable";
}
3.5 错误处理与降级
// ErrorBoundary 包裹子应用容器
class SubAppErrorBoundary extends React.Component<{name: string}, {hasError: boolean}> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error(`[SubApp] ${this.props.name} 渲染异常`, error);
// 上报监控
}
render() {
if (this.state.hasError) {
return (
<Result
status="error"
title={`${this.props.name} 模块加载异常`}
extra={<Button onClick={() => this.setState({ hasError: false })}>重试</Button>}
/>
);
}
return this.props.children;
}
}
// qiankun 加载超时处理
registerMicroApps(apps, {
beforeLoad: [async (app) => {
const timeout = 10000;
return Promise.race([
Promise.resolve(),
new Promise((_, reject) => setTimeout(() => reject(new Error(`加载超时 ${app.name}`)), timeout)),
]);
}],
});
3.6 性能优化
| 优化项 |
做法 |
效果 |
| 预加载 |
主应用 idle 时预加载其他子应用 JS/CSS |
路由切换瞬间渲染 |
| 预请求 |
子应用数据和 JS 并行加载(HTML Entry 提前解析 API 路径) |
减少瀑布请求 |
| 按需加载 |
仅加载当前路由匹配的子应用 |
首屏体积最小 |
| keep-alive |
wujie/micro-app 保活模式,子应用切换不销毁 |
切换零等待(内存消耗增加) |
| 异步分包 |
MF 模式下按页面分包远程模块 |
粒度最细 |
// qiankun 预加载 + 手动控制
import { prefetchApps } from 'qiankun';
// 首屏加载完成后,空闲时预加载高频子应用
window.requestIdleCallback(() => {
prefetchApps([
{ name: 'order', entry: '//localhost:3001' },
{ name: 'user', entry: '//localhost:3002' },
]);
});
4. 微前端 vs 替代方案
什么时候不需要微前端
- 小团队(<10人):单体应用足够,微前端反而增加架构复杂度
- 单一业务线:无多团队协作冲突,模块化 + Monorepo 即可
- 技术栈统一:无渐进升级需求,Monorepo 共享 lint/CI/测试更高效
- 构建速度未成瓶颈:5 分钟内的构建不值得引入运行时开销
替代方案对比
| 方案 |
优势 |
劣势 |
适用场景 |
| Monorepo(pnpm workspace / turborepo) |
代码共享简单、统一 lint/test/CI、原子提交 |
构建慢(随代码量线性增长)、部署耦合 |
技术栈统一、单团队 |
| 模块化拆分 |
代码层面解耦、tree-shaking 友好 |
统一构建部署、无法独立发布 |
代码组织阶段 |
| iframe |
天然硬隔离、零改造、接入最快 |
体验差(滚动/URL 不同步/弹窗溢出)、通信麻烦、SEO 不友好 |
嵌入第三方不可信内容、老系统兜底 |
| Web Components |
标准技术、框架无关 |
生态不成熟、SSR 难、复杂场景性能问题 |
UI 组件库跨框架 |
决策树
需要拆前端么?
├─ 小团队 / 单一业务 / 构建 < 3min → 不需要,Monorepo 或模块化即可
└─ 多团队 / 多业务线 / 构建 > 5min
├─ 是否要求独立部署?
│ ├─ 否 → Monorepo + 模块化
│ └─ 是
│ ├─ 是否技术栈无关?
│ │ ├─ 否(全部 React) → Module Federation(模块级)或 qiankun(应用级)
│ │ └─ 是
│ │ ├─ 安全隔离要求高?→ wujie(iframe 硬隔离)
│ │ ├─ 接入成本优先?→ micro-app(一行标签)
│ │ └─ 生态成熟优先?→ qiankun(社区资源最多)
│ └─ 还需要考虑
│ ├─ 组件级共享 > 应用级组合 → Module Federation
│ └─ 需要 IE11 兼容 → qiankun(SnapshotSandbox)
5. 参考资源