微前端架构实践
你有没有遇到过这种情况:
公司有一个运营了3年的后台管理系统,核心模块有十几个,团队从2个人涨到了8个人。每次发版都像拆炸弹——改了订单模块的代码,支付模块挂了;升级了 React 版本,老模块集体白屏。
我当时就想:能不能把一个大前端拆成好几个小前端,各团队独立开发、独立部署?
这就是微前端要解决的问题。
为什么要搞微前端
先说清楚,微前端不是银弹。如果你的项目就两三个人维护,一个 SPA 打天下完全没问题。
但下面这些场景,微前端确实能帮上大忙:
场景一:老项目渐进式迁移
你有个 jQuery 老系统,想逐步迁移到 React/Vue。用微前端可以把老模块和新模块并存,一个模块一个模块地替换,不用推倒重来。
场景二:多团队协同开发
8个团队,各自负责不同的业务模块。如果共享一个代码仓库,合并冲突能让你怀疑人生。微前端让每个团队有自己的仓库、自己的 CI/CD 流水线。
场景三:独立部署
订单团队改了个小 bug,不想等其他模块一起发版。微前端下,每个子应用可以独立部署,互不影响。
场景四:Agent 系统的插件化
这个比较有意思。假设你在做一个 Agent 平台,主应用负责对话和调度,但 Agent 的能力模块(知识库管理、工具市场、工作流编排)需要动态加载。用微前端的思想,每个 Agent 插件就是一个独立的子应用,可以热插拔。
方案对比:选谁?
微前端方案不少,但真正能打的就三家:qiankun、Module Federation、single-spa。我们一个一个看。
single-spa:元老级方案
single-spa 是微前端概念的先驱,核心思路是在运行时动态加载不同的前端应用。
// single-spa 注册子应用
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@myorg/order-app',
app: () => System.import('@myorg/order-app'),
activeWhen: '/order',
});
registerApplication({
name: '@myorg/payment-app',
app: () => System.import('@myorg/payment-app'),
activeWhen: '/payment',
});
start();
优点:轻量、框架无关、社区成熟。
缺点:JS 沙箱和样式隔离需要自己搞,没有开箱即用的沙箱机制。适合对微前端理解比较深的团队,能自己掌控全局。
qiankun:蚂蚁出品,国内主流
qiankun 基于 single-spa,但加上了 JS 沙箱、CSS 隔离、预加载等能力。对国内团队来说,文档是中文的,社区问题也好搜,上手成本最低。
// qiankun 主应用注册子应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'order-app',
entry: '//localhost:7100', // 子应用开发地址
container: '#subapp-container',
activeRule: '/order',
props: {
token: getToken(),
onGlobalStateChange: (state: any) => {
console.log('主应用状态变更:', state);
},
},
},
{
name: 'payment-app',
entry: '//localhost:7101',
container: '#subapp-container',
activeRule: '/payment',
},
]);
start({
prefetch: 'all', // 预加载所有子应用
sandbox: {
experimentalStyleIsolation: true, // CSS 隔离
},
});
qiankun 的核心能力:
- JS 沙箱:通过 Proxy 拦截 window 上的属性读写,防止子应用污染全局
- CSS 隔离:给子应用的样式加上作用域前缀(
__qiankun_stylelight_xxx__) - 生命周期管理:子应用需要导出 bootstrap、mount、unmount 三个生命周期
Module Federation:Webpack 5 原生方案
Module Federation(模块联邦)是 Webpack 5 引入的特性,核心思想是让不同构建工具打包的应用在运行时共享模块。
// webpack.config.js - host 应用
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
orderApp: 'orderApp@http://localhost:7100/remoteEntry.js',
paymentApp: 'paymentApp@http://localhost:7101/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
// host 应用中使用远程模块
const OrderModule = React.lazy(() => import('orderApp/OrderPage'));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<OrderModule />
</Suspense>
);
}
// 子应用 webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'orderApp',
filename: 'remoteEntry.js',
exposes: {
'./OrderPage': './src/pages/OrderPage',
'./orderUtils': './src/utils/orderUtils',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
Module Federation 的优势是"共享"——React、lodash 这些公共依赖只加载一次,不需要每个子应用都打一份。
三者对比
+------------------+----------+-------------------+-------------------+
| 维度 | qiankun | Module Federation | single-spa |
+------------------+----------+-------------------+-------------------+
| JS 沙箱 | 内置 | 无 | 需手动实现 |
| CSS 隔离 | 内置 | 无 | 需手动实现 |
| 依赖共享 | 一般 | 优秀(singleton) | 一般 |
| 独立部署 | 支持 | 支持 | 支持 |
| 构建工具无关 | 支持 | 仅 Webpack 5+ | 支持 |
| 子应用技术栈无关 | 支持 | 有限 | 支持 |
| 接入成本 | 低 | 中 | 高 |
| 性能 | 中 | 高 | 高 |
+------------------+----------+-------------------+-------------------+
怎么选? 如果你是多团队协同、技术栈不统一,qiankun 是最稳妥的选择。如果你全是 React 项目、追求性能,Module Federation 更香。如果团队技术实力强、需要极致控制,single-spa 够用。
架构设计
说完了方案选型,来看看整体架构长什么样。
+-------------------------------------------------------------+
| 主应用 (Main Shell) |
| +-------------+ +------------------+ +-----------------+ |
| | 全局路由 | | 全局状态管理 | | 通信总线 | |
| +------+------+ +--------+---------+ +--------+--------+ |
| | | | |
| +------v----------------v----------------------v--------+ |
| | 子应用挂载容器 (Container) | |
| +------+------+----------------+----------------+-------+ |
| | | | |
| +------v------+ +------------v---+ +--------v--------+ |
| | 订单子应用 | | 支付子应用 | | Agent 插件子应用 | |
| | (React 18) | | (Vue 3) | | (React 18) | |
| | 独立仓库 | | 独立仓库 | | 独立仓库 | |
| | 独立CI/CD | | 独立CI/CD | | 独立CI/CD | |
| +-------------+ +----------------+ +-----------------+ |
+-------------------------------------------------------------+
主应用(也叫 Shell 应用)负责三件事:
- 全局路由:根据 URL 决定加载哪个子应用
- 全局状态管理:用户信息、权限、主题等全局共享的状态
- 通信总线:子应用之间不能直接通信,必须通过主应用中转
Agent 系统的微前端模块化
这块是我实际做过的,特别有意思。
假设你要做一个 Agent 平台,需求大概是这样的:
- 主应用负责对话交互、Agent 调度
- 知识库管理是一个独立模块(RAG 相关)
- 工具市场是一个独立模块(MCP 工具管理)
- 工作流编排是一个独立模块(可视化拖拽)
- 监控面板是一个独立模块(调用链追踪)
这些模块的技术栈可能不一样:知识库用 React + D3 做可视化,工作流用 Vue3 + LogicFlow 做拖拽,监控面板用 React + ECharts。
用微前端来搞:
+--------------------------------------------------------------+
| Agent Platform Shell |
| |
| +------------------+ +----------------------------------+ |
| | 侧边栏导航 | | 子应用容器 | |
| | | | | |
| | - 对话 (内置) | | +----------------------------+ | |
| | - 知识库 (MF) | | | 知识库管理子应用 | | |
| | - 工具市场 (MF) | | | React + D3 可视化 | | |
| | - 工作流 (MF) | | +----------------------------+ | |
| | - 监控 (MF) | | | |
| +------------------+ +----------------------------------+ |
| |
| +----------------------------------------------------------+|
| | 全局状态层 ||
| | AgentContext | UserContext | ConfigContext ||
| +----------------------------------------------------------+|
+--------------------------------------------------------------+
主应用通过 Module Federation 动态加载 Agent 插件:
// Agent 平台主应用 - 插件注册系统
interface AgentPlugin {
name: string;
entry: string; // remoteEntry.js 地址
icon: string;
route: string;
permissions: string[]; // 需要的权限
}
// 插件配置(可以后端下发,支持动态更新)
const pluginRegistry: AgentPlugin[] = [
{
name: 'knowledge-base',
entry: 'http://localhost:7200/remoteEntry.js',
icon: 'database',
route: '/knowledge',
permissions: ['knowledge:read', 'knowledge:write'],
},
{
name: 'tool-market',
entry: 'http://localhost:7201/remoteEntry.js',
icon: 'tools',
route: '/tools',
permissions: ['tools:read'],
},
{
name: 'workflow-editor',
entry: 'http://localhost:7202/remoteEntry.js',
icon: 'flow',
route: '/workflow',
permissions: ['workflow:read', 'workflow:write'],
},
];
具体实现
qiankun 实现:子应用生命周期
子应用必须导出三个生命周期函数,qiankun 才能正确挂载和卸载。
// 子应用入口文件 - lifecycle.ts
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
let root: ReactDOM.Root | null = null;
// 子应用独立运行时
function render(props: any) {
const { container } = props;
const dom = container
? container.querySelector('#subapp-root')
: document.getElementById('subapp-root');
root = ReactDOM.createRoot(dom);
root.render(<App />);
}
// 独立运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
render({});
}
// ---- qiankun 生命周期 ----
export async function bootstrap() {
console.log('[子应用] bootstrap');
}
export async function mount(props: any) {
console.log('[子应用] mount, props:', props);
// 可以在这里接收主应用传过来的数据
const { token, userInfo } = props;
// 初始化全局状态监听
props.onGlobalStateChange((state: any) => {
console.log('[子应用] 主应用状态变更:', state);
});
render(props);
}
export async function unmount() {
console.log('[子应用] unmount');
root?.unmount();
root = null;
}
export async function update(props: any) {
console.log('[子应用] update:', props);
}
qiankun 沙箱机制详解
qiankun 的 JS 沙箱有两种模式:快照沙箱和代理沙箱。
// qiankun 沙箱源码简化版
// 快照沙箱 - 记录 window 上的变更,卸载时恢复
class SnapshotSandbox {
private snapshot: Map<string, any> = new Map();
private modifiedProps: Set<string> = new Set();
activate() {
// 激活时,对 window 做一次快照
for (const key of Object.keys(window)) {
this.snapshot.set(key, (window as any)[key]);
}
}
deactivate() {
// 卸载时,恢复 window 到激活前的状态
for (const key of this.modifiedProps) {
(window as any)[key] = this.snapshot.get(key);
}
}
// 拦截 set 操作,记录修改
set(key: string, value: any) {
this.modifiedProps.add(key);
(window as any)[key] = value;
}
}
// 代理沙箱 - 用 Proxy 隔离,每个子应用有自己的 window 副本
class ProxySandbox {
private proxy: WindowProxy;
private active = true;
constructor() {
const rawWindow = window;
const fakeWindow: Record<string, any> = {};
this.proxy = new Proxy(fakeWindow, {
get(target, key) {
// 优先从 fakeWindow 取,没有则从真实 window 取
if (key in target) return (target as any)[key];
return Reflect.get(rawWindow, key);
},
set(target, key, value) {
if (this.active) {
(target as any)[key] = value;
}
return true;
},
});
}
}
注意一个关键区别:快照沙箱只支持一个子应用同时运行,代理沙箱支持多个子应用并行。qiankun 默认在多实例模式下使用代理沙箱。
Module Federation 实现:Vite 版本
如果你用 Vite 而不是 Webpack,可以用 @originjs/vite-plugin-federation 实现类似效果。
// vite.config.ts - host 应用
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'host',
remotes: {
orderApp: 'http://localhost:7100/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
});
// vite.config.ts - 子应用
import { defineConfig } from 'vite';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
federation({
name: 'orderApp',
filename: 'remoteEntry.js',
exposes: {
'./OrderPage': './src/pages/OrderPage.tsx',
},
shared: ['react', 'react-dom'],
}),
],
build: {
target: 'esnext', // 必须设置为 esnext
},
});
通信机制
子应用之间不能直接 import 对方的代码,那怎么通信?有几种方式:
方式一:CustomEvent(推荐)
// 主应用 - 事件总线
class MicroEventBus {
private events = new Map<string, Set<Function>>();
on(event: string, callback: Function) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event)!.add(callback);
}
emit(event: string, data?: any) {
window.dispatchEvent(
new CustomEvent('micro:before-event', { detail: { event, data } })
);
}
}
// 子应用监听
window.addEventListener('micro:before-event', ((e: CustomEvent) => {
const { event, data } = e.detail;
if (event === 'agent:thinking') {
console.log('Agent 正在思考...', data);
}
}) as EventListener);
// 子应用发送
window.dispatchEvent(
new CustomEvent('micro:before-event', {
detail: { event: 'tool:executed', data: { toolId: 'search', result: '...' } },
})
);
方式二:qiankun 全局状态
// 主应用
import { initGlobalState } from 'qiankun';
const initialState = {
user: { id: 1, name: 'admin' },
theme: 'dark',
agentStatus: 'idle', // idle | thinking | executing | error
};
const actions = initGlobalState(initialState);
// 主应用自己也可以监听
actions.onGlobalStateChange((state, prev) => {
console.log('状态变更:', state);
if (state.agentStatus !== prev.agentStatus) {
// 更新 UI
}
});
// 子应用在 mount 时接收
export async function mount(props: any) {
props.onGlobalStateChange((state: any) => {
// 子应用响应主应用状态变更
});
// 子应用也可以修改全局状态(谨慎使用)
props.setGlobalState({ agentStatus: 'thinking' });
}
方式三:URL 参数传递
// 简单但有效,适合一次性数据传递
// 主应用跳转时带上参数
window.location.href = '/order?agentId=123&conversationId=456';
// 子应用读取参数
const params = new URLSearchParams(window.location.search);
const agentId = params.get('agentId');
共享依赖
子应用都用了 React,不可能每个子应用都打一份。Module Federation 的 shared 配置就是解决这个问题的。
// 主应用和子应用都需要配置 shared
shared: {
react: {
singleton: true, // 只加载一次
requiredVersion: '^18.0.0',
eager: true, // 主应用设置为 true,子应用不设置
},
'react-dom': {
singleton: true,
requiredVersion: '^18.0.0',
eager: true,
},
// 不常用的库可以按需加载
lodash: {
singleton: true,
requiredVersion: '^4.17.0',
},
}
坑在这里:如果 React 版本不一致怎么办?
// 主应用用 React 18.2,子应用用 React 18.3
// Module Federation 会选一个版本,可能导致兼容问题
// 最好的做法:统一 React 版本
// 可以在主应用配置中加版本范围
shared: {
react: {
singleton: true,
requiredVersion: '>=18.0.0 <19.0.0', // 宽松的版本范围
},
}
对于 qiankun,共享依赖需要用其他方式:
// 主应用的 webpack externals 配置
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};
// 主应用 HTML 模板引入 CDN
// <script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
部署策略
微前端的部署是独立的,但需要约定好接口。
子应用独立部署
# .github/workflows/deploy-order-app.yml
name: Deploy Order App
on:
push:
branches: [main]
paths: ['apps/order-app/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: cd apps/order-app && npm ci && npm run build
- name: Deploy to CDN
run: |
aws s3 sync dist/ s3://micro-apps-cdn/order-app/
aws cloudfront create-invalidation \
--distribution-id E1234 \
--paths "/order-app/*"
Nginx 配置
server {
listen 80;
server_name platform.example.com;
# 主应用
location / {
root /data/www/shell;
index index.html;
try_files $uri $uri/ /index.html;
}
# 子应用 - 订单模块
location /order/ {
root /data/www/sub-apps;
try_files $uri $uri/ /order/index.html;
}
# 子应用 - Agent 插件
location /agent-plugin/ {
root /data/www/sub-apps;
try_files $uri $uri/ /agent-plugin/index.html;
}
# 子应用静态资源(独立 CDN)
location /static/order-app/ {
alias /data/cdn/order-app/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
踩坑记录
坑一:子应用路由冲突
子应用都用 React Router,各自定义了 / 路由,结果切换子应用时 URL 没变。
原因:qiankun 的路由劫持和 React Router 的 history 冲突了。
解决:子应用的路由必须加上自己的前缀。
// 子应用路由配置 - 错误
<BrowserRouter>
<Route path="/" element={<OrderList />} />
</BrowserRouter>
// 子应用路由配置 - 正确
<BrowserRouter basename="/order">
<Route path="/" element={<OrderList />} />
<Route path="/detail/:id" element={<OrderDetail />} />
</BrowserRouter>
坑二:全局样式污染
子应用 A 用了 * { box-sizing: border-box },子应用 B 的布局直接崩了。
解决:CSS 隔离要开起来,同时子应用不要用全局选择器。
// qiankun 开启 CSS 隔离
start({
sandbox: {
experimentalStyleIsolation: true,
},
});
// 子应用内部用 CSS Modules
// OrderList.module.css
.orderContainer { ... }
.orderItem { ... }
// 使用
import styles from './OrderList.module.css';
<div className={styles.orderContainer}>...</div>
坑三:子应用静态资源加载失败
子应用打包后的图片、字体等静态资源路径是相对路径,加载时 404。
解决:子应用的 publicPath 必须设置为绝对路径。
// 子应用入口文件顶部
if (window.__POWERED_BY_QIANKUN__) {
// 动态设置 publicPath
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// 或者在 webpack 配置中
module.exports = {
output: {
publicPath: '//cdn.example.com/order-app/',
// 开发环境
publicPath: 'http://localhost:7100/',
},
};
坑四:子应用白屏(常见但难查)
白屏原因很多,最常见的几个:
- 子应用没导出 bootstrap/mount/unmount
- 子应用报错导致渲染失败(但错误被沙箱吞了)
- 容器 DOM 节点不存在
// 调试方法:在主应用中开启 debug
import { start } from 'qiankun';
// 开发环境加日志
start({
debug: true, // 开启调试模式
});
// 子应用加 try-catch
export async function mount(props: any) {
try {
render(props);
} catch (err) {
console.error('[子应用 mount 失败]', err);
// 可以上报监控系统
}
}
坑五:Agent 插件动态加载的安全问题
Agent 平台允许动态加载插件,但恶意插件可能偷数据。
// 插件加载前做安全检查
async function loadPlugin(plugin: AgentPlugin) {
// 1. 检查来源是否可信
if (!isTrustedOrigin(plugin.entry)) {
throw new Error('不可信的插件来源');
}
// 2. 检查插件签名(如果有的话)
const manifest = await fetch(`${plugin.entry}/manifest.json`);
const manifestData = await manifest.json();
if (!verifySignature(manifestData)) {
throw new Error('插件签名验证失败');
}
// 3. 加载插件
await import(/* webpackIgnore: true */ plugin.entry);
}
// 4. iframe 沙箱(更严格的隔离)
// 对于不信任的插件,可以用 iframe 嵌套
<iframe
src={plugin.entry}
sandbox="allow-scripts allow-same-origin"
style={{ width: '100%', height: '100%', border: 'none' }}
/>