跳到主要内容

微前端架构实践

你有没有遇到过这种情况:

公司有一个运营了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 应用)负责三件事:

  1. 全局路由:根据 URL 决定加载哪个子应用
  2. 全局状态管理:用户信息、权限、主题等全局共享的状态
  3. 通信总线:子应用之间不能直接通信,必须通过主应用中转

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
// &lt;script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"&gt;&lt;/script&gt;
// &lt;script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"&gt;&lt;/script&gt;

部署策略

微前端的部署是独立的,但需要约定好接口。

子应用独立部署

# .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 冲突了。

解决:子应用的路由必须加上自己的前缀。

// 子应用路由配置 - 错误
&lt;BrowserRouter&gt;
&lt;Route path="/" element={&lt;OrderList /&gt;} /&gt;
&lt;/BrowserRouter&gt;

// 子应用路由配置 - 正确
&lt;BrowserRouter basename="/order"&gt;
&lt;Route path="/" element={&lt;OrderList /&gt;} /&gt;
&lt;Route path="/detail/:id" element={&lt;OrderDetail /&gt;} /&gt;
&lt;/BrowserRouter&gt;

坑二:全局样式污染

子应用 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';
&lt;div className={styles.orderContainer}&gt;...&lt;/div&gt;

坑三:子应用静态资源加载失败

子应用打包后的图片、字体等静态资源路径是相对路径,加载时 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/',
},
};

坑四:子应用白屏(常见但难查)

白屏原因很多,最常见的几个:

  1. 子应用没导出 bootstrap/mount/unmount
  2. 子应用报错导致渲染失败(但错误被沙箱吞了)
  3. 容器 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 嵌套
&lt;iframe
src={plugin.entry}
sandbox="allow-scripts allow-same-origin"
style={{ width: '100%', height: '100%', border: 'none' }}
/&gt;

参考资料