React 微前端架构实践
React 微前端架构实践
微前端(Micro Frontends)是将大型前端应用拆分为多个独立开发、独立部署、独立运行的小型应用,再在运行时把它们组合成一个完整产品的架构模式。它把后端「微服务」的思想搬到了浏览器端。
一个类比先建立直觉
把一个巨型电商网站想象成一座大型商场:
顾客走进商场,感觉像逛「一家店」,其实背后是几十家独立经营的店铺协同工作。这就是微前端追求的体验:用户无感知的一体化,团队高度自治的分散化。
微前端核心概念
什么是微前端:
为什么需要微前端(解决什么痛点):
优势:
挑战(也是本文重点要解决的问题):
主流实现方案原理
方案一:iframe
最古老也最简单的方案。每个微应用是一个独立页面,容器用 `
原理:浏览器原生的 iframe 提供了天然的、彻底的隔离——独立的 window、document、样式作用域、JS 执行环境。
优点:隔离性最强,几乎零成本接入,任何技术栈都能塞进去。
缺点:URL 不同步、刷新丢状态、弹窗被限制在 iframe 内、无法共享依赖(每个 iframe 都要重新加载一份 React)、通信只能靠 postMessage、SEO 与无障碍差、双滚动条等体验问题。
方案二:Web Components + import maps
用浏览器原生的自定义元素(Custom Elements)和 Shadow DOM 封装微应用,用 import maps 管理依赖版本。
原理:每个微应用编译成一个自定义元素(如 `
<!-- 容器 HTML 中用 import maps 声明共享依赖 -->
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react-dom": "https://esm.sh/react-dom@18.2.0"
}
}
</script>
<!-- 直接以自定义元素的方式使用微应用 -->
<order-app data-user-id="1024"></order-app>优点:标准化、面向未来、天然样式隔离。缺点:浏览器兼容与 polyfill、React 事件与 Shadow DOM 的边界问题、生态工具偏少。
方案三:single-spa
微前端领域的「元框架」,负责在运行时注册多个应用,并管理它们的生命周期(load / bootstrap / mount / unmount)。
原理:single-spa 本身不关心你用什么框架,它要求每个微应用导出一组生命周期函数,然后根据路由或条件决定挂载哪个应用。
方案四:qiankun(蚂蚁开源,基于 single-spa)
在 single-spa 之上封装了 HTML Entry(直接用子应用的 index.html 作为入口)、JS 沙箱(Proxy / 快照沙箱隔离全局变量)、样式隔离、资源预加载 等开箱即用的能力,是国内中后台项目最流行的方案。
方案五:Module Federation(Webpack 5 / Rspack)
Webpack 5 提供的模块联邦,是当下最主流、最「前端原生」的方案。
原理:允许一个构建产物(host)在运行时动态加载另一个构建产物(remote)暴露出来的模块,并且能在多个应用之间共享依赖(如只加载一份 React)。它模糊了「构建时」和「运行时」的边界,让不同独立构建的应用像引用本地模块一样引用彼此的代码。
代码示例
Module Federation:Remote(提供方)配置
remote 应用负责把自己的组件暴露出去。以一个「订单微应用」为例:
// order-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'production',
output: {
// publicPath 必须是绝对地址,供 host 远程加载
publicPath: 'https://cdn.example.com/order-app/',
uniqueName: 'orderApp',
},
plugins: [
new ModuleFederationPlugin({
// 该 remote 的唯一名字,host 会用它引用
name: 'orderApp',
// 远程入口文件名,host 通过它拿到 manifest
filename: 'remoteEntry.js',
// 对外暴露的模块:键是别名,值是真实路径
exposes: {
'./OrderList': './src/components/OrderList',
'./OrderDetail': './src/components/OrderDetail',
'./store': './src/store/orderStore',
},
// 共享依赖:singleton 保证全局只有一份 React
shared: {
react: {
singleton: true,
requiredVersion: deps.react,
eager: false,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
},
}),
],
};Module Federation:Host(消费方)配置
host(基座应用)声明要消费哪些 remote:
// shell-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'production',
plugins: [
new ModuleFederationPlugin({
name: 'shell',
// remotes 语法: 别名: '远程name@远程entry地址'
remotes: {
orderApp: 'orderApp@https://cdn.example.com/order-app/remoteEntry.js',
userApp: 'userApp@https://cdn.example.com/user-app/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
},
}),
],
};Module Federation:在 Host 中动态加载 Remote 组件
配合 React.lazy 与 Suspense,实现按需、懒加载远程组件,并处理加载失败:
// shell-app/src/pages/OrdersPage.tsx
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
// 声明远程模块类型(可放到 remotes.d.ts)
// declare module 'orderApp/OrderList';
// React.lazy 会在渲染时才真正去拉取 remoteEntry
const RemoteOrderList = React.lazy(() => import('orderApp/OrderList'));
function Fallback() {
return <div>订单模块加载失败,请稍后重试</div>;
}
export default function OrdersPage() {
return (
<ErrorBoundary FallbackComponent={Fallback}>
<Suspense fallback={<div>订单模块加载中…</div>}>
<RemoteOrderList pageSize={20} />
</Suspense>
</ErrorBoundary>
);
}如果需要完全运行时、不写进 webpack 配置的动态加载,可以用底层的联邦 API:
// 运行时动态加载任意 remote,无需构建时声明
async function loadRemote(remoteUrl: string, scope: string, module: string) {
// 1. 动态插入 remoteEntry 脚本
await new Promise<void>((resolve, reject) => {
const el = document.createElement('script');
el.src = remoteUrl;
el.onload = () => resolve();
el.onerror = () => reject(new Error('加载 remoteEntry 失败'));
document.head.appendChild(el);
});
// 2. 初始化共享作用域
// @ts-ignore webpack 运行时全局变量
await __webpack_init_sharing__('default');
// @ts-ignore 从 window 上取到 remote 容器
const container = window[scope];
// @ts-ignore 用共享作用域初始化容器
await container.init(__webpack_share_scopes__.default);
// 3. 取出暴露的模块工厂并执行
const factory = await container.get(module);
return factory();
}
// 使用
const mod = await loadRemote(
'https://cdn.example.com/order-app/remoteEntry.js',
'orderApp',
'./OrderList',
);
const OrderList = mod.default;qiankun:注册微应用与启动
qiankun 的 host 端非常简洁,用 `registerMicroApps` 注册、`start` 启动:
// shell-app/src/micro.ts
import { registerMicroApps, start, addGlobalUncaughtErrorHandler } from 'qiankun';
registerMicroApps(
[
{
name: 'order-app',
entry: '//localhost:7101', // 直接用子应用的 HTML entry
container: '#subapp-viewport',
activeRule: '/order', // 路由匹配到 /order 时激活
props: { token: () => localStorage.getItem('token') },
},
{
name: 'user-app',
entry: '//localhost:7102',
container: '#subapp-viewport',
activeRule: '/user',
},
],
{
beforeLoad: (app) => console.log('before load', app.name),
afterMount: (app) => console.log('mounted', app.name),
},
);
addGlobalUncaughtErrorHandler((event) => {
console.error('微应用运行时异常', event);
});
start({
prefetch: 'all', // 预加载所有子应用资源
sandbox: { strictStyleIsolation: false, experimentalStyleIsolation: true },
singular: true, // 同一时刻只挂载一个子应用
});qiankun:React 子应用改造导出生命周期
子应用需要导出 bootstrap / mount / unmount 三个生命周期,并适配 qiankun 的 webpack 配置:
// order-app/src/index.tsx
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import App from './App';
let root: Root | null = null;
function render(props: { container?: HTMLElement } = {}) {
const { container } = props;
const el = container
? container.querySelector('#root')
: document.getElementById('root');
root = createRoot(el as HTMLElement);
root.render(<App />);
}
// 独立运行时(非 qiankun 环境)直接渲染
// @ts-ignore qiankun 注入的全局标识
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log('order-app bootstraped');
}
export async function mount(props: any) {
console.log('order-app mount', props);
render(props);
}
export async function unmount() {
root?.unmount();
root = null;
}子应用的 webpack 需要输出 UMD 并允许跨域:
// order-app/webpack.config.js(qiankun 版)
module.exports = {
output: {
library: 'order-app',
libraryTarget: 'umd', // 让 qiankun 能拿到生命周期导出
globalObject: 'window',
publicPath: 'auto',
},
devServer: {
port: 7101,
headers: { 'Access-Control-Allow-Origin': '*' }, // 允许基座跨域拉取
},
};single-spa:注册应用
single-spa 更底层,需要手动写生命周期适配层:
// root-config/src/root.config.ts
import { registerApplication, start } from 'single-spa';
registerApplication({
name: '@team/order',
// 通过 SystemJS 动态导入子应用打包产物
app: () => System.import('@team/order'),
activeWhen: (location) => location.pathname.startsWith('/order'),
customProps: { authToken: 'xxx' },
});
registerApplication({
name: '@team/user',
app: () => System.import('@team/user'),
activeWhen: '/user',
});
start({ urlRerouteOnly: true });React 子应用用 single-spa-react 适配器包一层即可导出生命周期:
// order-app/src/spa.tsx
import React from 'react';
import ReactDOMClient from 'react-dom/client';
import singleSpaReact from 'single-spa-react';
import App from './App';
const lifecycles = singleSpaReact({
React,
ReactDOMClient,
rootComponent: App,
errorBoundary(err) {
return <div>订单应用出错:{String(err)}</div>;
},
});
export const { bootstrap, mount, unmount } = lifecycles;微应用间通信:事件总线
最通用的解耦通信方式。基于发布订阅模式,任意微应用都能收发消息:
// shared/eventBus.ts —— 挂在容器上,供所有微应用共用
type Handler = (payload: unknown) => void;
class EventBus {
private events = new Map<string, Set<Handler>>();
on(type: string, handler: Handler) {
if (!this.events.has(type)) this.events.set(type, new Set());
this.events.get(type)!.add(handler);
// 返回取消订阅函数,避免内存泄漏
return () => this.off(type, handler);
}
off(type: string, handler: Handler) {
this.events.get(type)?.delete(handler);
}
emit(type: string, payload?: unknown) {
this.events.get(type)?.forEach((h) => h(payload));
}
}
// 挂到全局,让所有微应用共享同一个实例
// @ts-ignore
window.__EVENT_BUS__ = window.__EVENT_BUS__ || new EventBus();
// @ts-ignore
export const eventBus: EventBus = window.__EVENT_BUS__;在 React 微应用中封装成 hook 使用:
// hooks/useEventBus.ts
import { useEffect } from 'react';
import { eventBus } from '../shared/eventBus';
export function useEventBus(type: string, handler: (p: unknown) => void) {
useEffect(() => {
const unsubscribe = eventBus.on(type, handler);
return unsubscribe; // 组件卸载时自动解绑
}, [type, handler]);
}
// 用户微应用登录成功后广播
eventBus.emit('user:login', { userId: 1024, name: '张三' });
// 订单微应用监听登录事件刷新数据
function OrderList() {
useEventBus('user:login', (payload) => {
console.log('收到登录事件,刷新订单', payload);
});
return <div>订单列表</div>;
}微应用间通信:共享状态
对于需要持续同步的状态(如用户信息、主题、语言),可用一个轻量的可观察 store:
// shared/globalStore.ts
type Listener<T> = (state: T) => void;
function createStore<T extends object>(initial: T) {
let state = initial;
const listeners = new Set<Listener<T>>();
return {
getState: () => state,
setState(patch: Partial<T>) {
state = { ...state, ...patch };
listeners.forEach((fn) => fn(state));
},
subscribe(fn: Listener<T>) {
listeners.add(fn);
return () => listeners.delete(fn);
},
};
}
// @ts-ignore 全局唯一实例
window.__GLOBAL_STORE__ =
// @ts-ignore
window.__GLOBAL_STORE__ ||
createStore({ theme: 'light', locale: 'zh-CN', user: null });
// @ts-ignore
export const globalStore = window.__GLOBAL_STORE__;qiankun 也内置了基于 props 的通信(`initGlobalState`):
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, theme: 'light' });
// 基座监听变化
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变化', prev, '->', state);
});
// 基座主动更新
actions.setGlobalState({ theme: 'dark' });
// 子应用在 mount 时会收到 props.onGlobalStateChange 与 props.setGlobalState
export async function mount(props: any) {
props.onGlobalStateChange((state: any) => console.log('子应用收到', state));
props.setGlobalState({ user: { id: 1 } });
}样式隔离方案
1)CSS Modules:CSS Modules 是 CSS 模块化解决方案,通过将 CSS 类名转换为唯一的哈希值来实现作用域隔离,避免样式冲突,支持组合、变量等特性,可以在 React 组件中直接导入使用。
// Button.module.css 中的 .primary 会被编译成 .Button_primary__a1b2c
import styles from './Button.module.css';
export function Button() {
return <button className={styles.primary}>下单</button>;
}2)Shadow DOM:Shadow DOM 是 Web Components 的核心特性,提供了完全的样式隔离,Shadow DOM 内部的样式不会影响外部,外部样式也不会影响内部,适合需要严格样式隔离的组件。
// 把整个微应用挂载进 Shadow Root,实现物理级样式隔离
function mountInShadow(host: HTMLElement, App: React.ComponentType) {
const shadow = host.attachShadow({ mode: 'open' });
const mountPoint = document.createElement('div');
shadow.appendChild(mountPoint);
createRoot(mountPoint).render(<App />);
}3)CSS-in-JS:CSS-in-JS 是将 CSS 样式写在 JavaScript 中的解决方案,主流库包括 styled-components 和 emotion,优点是组件样式与组件代码共存便于维护,且类名自动带唯一后缀天然隔离,缺点是运行时开销和可能的包体积增加。
4)BEM / 命名空间前缀:给每个微应用的所有样式加上唯一前缀(如 `.order-app__title`),是最朴素但兼容性最好的方案。qiankun 的 `experimentalStyleIsolation` 就是自动给子应用样式加 `div[data-qiankun="order-app"]` 前缀实现的。
JS 沙箱原理(qiankun 快照 / Proxy 沙箱)
为了防止子应用污染全局 window,qiankun 实现了沙箱。Proxy 沙箱的核心思想:
// 简化版 Proxy 沙箱:子应用对 window 的写操作只落到自己的 fakeWindow
function createProxySandbox() {
const fakeWindow: Record<string, unknown> = {};
const proxy = new Proxy(window, {
get(target, key: string) {
// 优先读子应用自己的值,其次读真实 window
return key in fakeWindow
? fakeWindow[key]
: (target as any)[key];
},
set(_target, key: string, value) {
// 写操作被拦截,只写进 fakeWindow,不污染真实 window
fakeWindow[key] = value;
return true;
},
});
return proxy;
}子应用卸载时,只需丢弃 fakeWindow 即可完全还原环境,多个子应用切换时互不干扰。
真实场景案例
案例一:大型企业中台多团队协作
某银行内部管理平台,包含风控、清算、客户管理、报表、运营等 8 个业务域,由 6 个团队维护。
案例二:遗留系统渐进式迁移
某电商后台是 2016 年的 AngularJS(1.x)巨石应用,无法一次性重写。
案例三:多技术栈共存的聚合门户
某集团把子公司各自独立的 React、Vue3、Svelte 系统聚合成统一门户。
各方案对比数据
以下数字为常见工程经验量级,用于横向对比,实际以项目实测为准。
| 方案 | 隔离性 | 依赖共享 | 运行时性能开销 | 学习成本 | 框架支持 | 适用场景 |
| --- | --- | --- | --- | --- | --- | --- |
| iframe | 极强(物理隔离) | 不支持(各自加载) | 高(重复加载依赖,内存约翻倍) | 极低 | 任意 | 第三方系统嵌入、强隔离需求 |
| Web Components | 强(Shadow DOM) | 靠 import maps | 中 | 中高 | 任意 | 标准化组件、跨栈聚合 |
| single-spa | 弱(需自行处理) | 需手动配置 | 低 | 高 | 多框架 | 多技术栈、渐进迁移 |
| qiankun | 中强(沙箱+样式隔离) | 支持(externals) | 中低 | 中 | React/Vue/Angular | 中后台多团队中台 |
| Module Federation | 中(需配合样式方案) | 原生支持、最优 | 低 | 中高 | 主要 React/Vue | 组件级共享、现代前端 |
依赖共享对体积的影响(以三个都用 React 18 + 组件库的子应用为例,仅示意量级):
| 策略 | 首屏 JS 总量 | 说明 |
| --- | --- | --- |
| 各自打包(无共享) | 约 1.8 MB | 每个应用各带一份 React 与组件库 |
| Module Federation 单例共享 | 约 1.1 MB | React/组件库全局仅一份,下降约 38% |
| iframe 独立加载 | 约 2.2 MB | 隔离最好但重复最严重 |
常见坑与排查
| 坑 | 现象 | 原因 | 解决 |
| --- | --- | --- | --- |
| React 多实例 | Hooks 报错 Invalid hook call | 共享未生效,加载了多份 React | Module Federation shared 设 singleton;qiankun 用 externals |
| 样式互相污染 | 子应用 A 的样式影响 B | 全局 CSS 未隔离 | 开启样式隔离 / CSS Modules / 加前缀 |
| 全局变量冲突 | 子应用切换后行为异常 | 未启用沙箱,window 被污染 | 开启 qiankun 沙箱或手动隔离 |
| 路由丢失 | 刷新子应用页面 404 | 服务端未配置 history fallback | Nginx/服务端配置 try_files 兜底 |
| publicPath 错误 | 子应用静态资源 404 | 打包 publicPath 不是绝对地址 | 设置绝对 publicPath 或 runtime publicPath |
| 内存泄漏 | 切换应用后内存持续上涨 | 事件监听、定时器未在 unmount 清理 | 生命周期中彻底清理副作用 |
| z-index 打架 | 弹窗被基座导航遮挡 | 层级管理无约定 | 统一 z-index 规范与挂载点 |
最佳实践
应用拆分原则:
容器(基座)应用职责:
微应用职责:
通信原则:
依赖共享策略:
CI/CD 与监控:
总结:方案选型对比表
| 你的场景 | 推荐方案 | 理由 |
| --- | --- | --- |
| 现代 React 技术栈、追求组件级复用与共享依赖 | Module Federation | 原生共享、运行时加载、体积最优 |
| 中后台多团队、要开箱即用的隔离与沙箱 | qiankun | HTML entry、沙箱、样式隔离一站式 |
| 多技术栈共存、需要渐进式迁移遗留系统 | single-spa | 框架无关、生命周期灵活 |
| 嵌入完全不可控的第三方系统、强隔离 | iframe | 物理隔离最彻底、接入成本最低 |
| 面向标准、跨栈组件复用 | Web Components + import maps | 浏览器原生、面向未来 |
核心结论: 微前端不是银弹,它用「运行时集成的复杂度」换「团队自治与独立部署的效率」。当团队规模大、业务域清晰、发布耦合成为瓶颈时,微前端收益显著;当应用规模小、团队人少时,直接上微前端往往是过度设计。选型上,现代 React 项目优先考虑 Module Federation,中后台多团队优先 qiankun,遗留系统渐进迁移优先 single-spa,强隔离嵌入优先 iframe。先把隔离(样式 + JS 沙箱)、通信(事件 + 共享状态)、依赖共享(singleton)这三件事想清楚,微前端才能真正落地而不是徒增复杂度。