微前端详解

一、微前端要解决什么问题

微前端(Micro-Frontends)是一种将前端应用拆分为多个独立子应用的架构模式,每个子应用可以独立开发、独立测试、独立部署,最终由主应用聚合为一个整体。

单体前端的问题

当一个前端项目增长到数十万行代码、上百个页面、几十人协作时,会出现以下问题:

  • 构建缓慢:一次 npm run build 耗时 5-10 分钟
  • 部署耦合:修改一个 1KB 的文案需要重新构建部署整个巨大应用
  • 技术栈锁定:项目早期选了 Angular 1,三年后想用 React 18,只能全量重写
  • 团队冲突:多个团队在同一个 Git 仓库中修改代码,合并冲突频繁
  • 回归风险:修改一个页面可能影响完全不相关的功能

微前端的解决思路

借鉴后端微服务的理念,将单体前端拆分为多个更小的子应用:

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────┐
│ 主应用容器(Base App) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 子应用 A │ │ 子应用 B │ │ 子应用 C │ │
│ │ (React) │ │ (Vue 3) │ │ (Svelte) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 子应用 D │ │ 子应用 E │ │
│ │ (Angular) │ │ (Vanilla) │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘

核心特性:

特性说明
技术栈无关子应用可以用不同的框架(React/Vue/Angular/原生)
独立开发每个子应用有独立的仓库、CI/CD 流水线
独立部署子应用可独立发布,不影响其他应用
增量升级老系统不用重写,新功能用新技术栈实现
按需加载用户只访问某个模块时,仅加载对应的子应用

二、技术方案对比

方案全景

方案原理样式隔离JS 沙箱通信生产案例
iframe浏览器原生隔离✅ 完全隔离✅ 天然隔离postMessage几乎都在用
Web ComponentsCustom Element✅ Shadow DOM❌ 无事件/CustomEventYouTube
Module FederationWebpack 5 插件❌ 需自行处理❌ 无共享模块/全局变量无数
qiankunsingle-spa + proxy✅ 实验性✅ Proxy 沙箱事件总线阿里系
micro-appCustom Element 封装✅ 自动隔离✅ Proxy 沙箱事件 + 数据京东
wujie(无界)Web Component + iframe✅ 双沙箱事件腾讯

1. iframe

优点:

  • 浏览器级别的完全隔离(样式、DOM、JS 执行环境)
  • 不需要任何改造,旧系统直接嵌入

缺点:

  • URL 不同步:刷新 iframe 页面,浏览器前进后退无法控制
  • 通信成本高postMessage 传递复杂数据结构需要序列化
  • 性能开销:每个 iframe 都有独立的执行环境,内存占用高
  • 弹窗/遮罩层仅限于 iframe 内:全屏弹窗无法覆盖主应用
  • SEO 不友好

适用场景:嵌入第三方不可控系统、遗留系统快速集成。

2. Web Components

使用 Custom Element + Shadow DOM 将子应用封装为自定义标签:

1
<micro-app name="order" url="http://order-app.com"></micro-app>
  • Shadow DOM 提供天然的样式隔离
  • Custom Element 提供生命周期(connectedCallback / disconnectedCallback)
  • 但 JS 沙箱缺失,第三方库可能污染全局

3. qiankun(基于 single-spa)

qiankun 是蚂蚁金服开源的微前端框架,基于 single-spa 封装,是目前国内使用最广泛的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 主应用
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
{
name: 'react-app',
entry: '//localhost:3001', // 子应用入口
container: '#sub-app', // 挂载容器
activeRule: '/react', // 路由激活规则
},
{
name: 'vue-app',
entry: '//localhost:4001',
container: '#sub-app',
activeRule: '/vue',
},
]);

start({ sandbox: { experimentalStyleIsolation: true } });

子应用需要导出生命周期钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 子应用 entry
let instance = null;

export async function bootstrap() {
// 初始化(只调用一次)
}

export async function mount(props) {
// 每次进入子应用时调用
instance = new Vue({
router,
render: h => h(App),
}).$mount('#app');
props.onGlobalStateChange?.((state) => { /* 监听全局状态 */ });
}

export async function unmount() {
// 每次离开子应用时调用
instance.$destroy();
instance = null;
}

qiankun 的核心机制

  • 沙箱机制:使用 Proxy 拦截 window 对象,子应用对 window 的修改在卸载时回滚
  • 样式隔离:将子应用样式包裹在命名空间下(实验性),或使用 Shadow DOM
  • 全局通信initGlobalState + onGlobalStateChange 实现应用间通信
  • 预加载:空闲时预加载子应用资源,提升切换体验

4. Module Federation(Webpack 5 / Rsbuild)

Module Federation 是 Webpack 5 内置的模块共享方案,不依赖运行时框架,通过构建时的模块联邦插件实现跨应用共享。

主应用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js (主应用)
const { ModuleFederationPlugin } = require('webpack').container;

new ModuleFederationPlugin({
name: 'base_app',
remotes: {
'reactApp': 'react_app@http://localhost:3001/remoteEntry.js',
'vueApp': 'vue_app@http://localhost:4001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
});

子应用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.config.js (React 子应用)
new ModuleFederationPlugin({
name: 'react_app',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/pages/OrderList',
'./OrderDetail': './src/pages/OrderDetail',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});

在主应用中加载远程模块

1
2
3
4
5
6
7
8
9
10
// 使用 React.lazy 动态加载远程组件
const OrderList = React.lazy(() => import('reactApp/OrderList'));

function App() {
return (
<Suspense fallback={<Loading />}>
<OrderList />
</Suspense>
);
}

Module Federation 的优势

  • 不依赖运行时框架,原生 Webpack 能力
  • 共享依赖:多个子应用可以共享同一个 React 实例,避免重复加载
  • 动态加载:支持运行时动态 remote
  • 构建时集成 vs 运行时集成:MF 更偏向构建时集成,但也有运行时能力

Module Federation 2.0(Module Federation)

最新版本支持:

1
2
3
4
// 动态加载 remote,无需预先配置
const federationRuntime = import('@module-federation/runtime');

const remote = await federationRuntime.loadRemote('react_app/OrderList');

5. micro-app(京东)

micro-app 基于 Web Components + Proxy 沙箱,使用体验接近 iframe 的简单性:

1
2
<!-- 主应用 -->
<micro-app name="order" url="http://localhost:3001/" baseroute="/order"></micro-app>
1
2
3
4
// 子应用适配(几乎零改造)
if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__;
}

特点:

  • 使用 Custom Element 加载子应用
  • Proxy 沙箱隔离 JS 作用域
  • 自动拦截样式,通过添加属性选择器实现样式隔离
  • 子应用改造极小

6. wujie 无界(腾讯)

wujie 结合了 iframe 的隔离能力和 Web Components 的便利性:

  • DOM 放在 Web Component 中,样式互不影响
  • JS 在 iframe 中运行,天然沙箱
  • 路由同步通过 history.pushState 代理
1
2
3
4
5
<WujieVue
name="order"
url="http://localhost:3001"
:props="{ token: userToken }"
></WujieVue>

方案选择建议

场景推荐方案理由
老系统 iframe 嵌入iframe零改造,快速集成
新项目整体架构Module Federation生态最完善,社区最活跃
已有 single-spa 项目qiankun平滑迁移,阿里生态
追求极简改造micro-app / wujie子应用改造量最小
跨技术栈组件共享Module Federation天然支持模块共享

三、核心挑战与解决方案

1. 样式隔离

方案一:CSS Modules / CSS-in-JS(子应用自治)

子应用各自使用 CSS Modules 或 styled-components,从构建层面避免冲突。这是最推荐的方案——不需要框架层面的样式隔离

方案二:命名空间前缀(scoped css)

1
2
3
/* 子应用所有类名加前缀 */
.react-app__header { ... }
.react-app__button { ... }

qiankun 的实验性样式隔离就是通过给样式表加前缀实现的。

方案三:Shadow DOM

真正的 DOM 隔离,但会导致:

  • 弹窗/Select 下拉无法穿越 Shadow DOM 边界
  • 第三方库如果向 body 挂载元素(如 Modal),样式会丢失
  • 事件重定向问题

方案四:micro-app / wujie 自动隔离

通过拦截样式表插入过程,给每个 CSS 规则加上属性选择器 [data-micro-app-{name}]

2. JS 沙箱

Proxy 沙箱(qiankun / micro-app)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 简化原理
const sandboxWindow = new Proxy(window, {
get(target, key) {
// 优先从沙箱上下文中读取
if (sandboxContext.has(key)) {
return sandboxContext.get(key);
}
return target[key];
},
set(target, key, value) {
// 修改记录在沙箱上下文中
sandboxContext.set(key, value);
return true;
},
});

// 子应用卸载时,删除沙箱上下文中新增的属性
function clearSandbox() {
sandboxContext.forEach((value, key) => {
delete window[key];
});
sandboxContext.clear();
}

Snapshot 沙箱(兼容 IE)

不支持 Proxy 时使用快照模式:

1
2
3
4
5
6
7
8
9
10
11
12
// 激活时保存 window 快照
const snapshot = {};
Object.keys(window).forEach(key => { snapshot[key] = window[key]; });

// 卸载时恢复
Object.keys(window).forEach(key => {
if (!(key in snapshot)) {
delete window[key]; // 删除子应用新增的属性
} else {
window[key] = snapshot[key]; // 恢复被修改的属性
}
});

iframe 沙箱(wujie)

wujie 使用隐藏 iframe 运行 JS,天然隔离,但需要代理 postMessage 实现 DOM 操作同步,实现成本较高。

3. 应用间通信

方案一:全局事件总线

1
2
3
4
5
6
7
8
9
10
11
12
13
// qiankun
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, token: '' });

// 子应用监听
export function mount(props) {
props.onGlobalStateChange((state) => {
console.log('新状态:', state);
}, true);

// 子应用修改全局状态
props.setGlobalState({ user: { name: 'Alice' } });
}

方案二:自定义事件

1
2
3
4
5
6
7
8
9
10
11
// 发送方
window.dispatchEvent(new CustomEvent('micro-app-message', {
detail: { type: 'USER_LOGIN', payload: { id: 1, name: 'Alice' } },
}));

// 接收方
window.addEventListener('micro-app-message', (e) => {
if (e.detail.type === 'USER_LOGIN') {
// 处理登录事件
}
});

方案三:共享模块(Module Federation)

通过 shared 配置共享状态管理库:

1
2
3
4
5
6
7
8
9
// 主应用暴露共享 store
new ModuleFederationPlugin({
shared: {
'@shared/store': {
singleton: true,
import: './src/store',
},
},
});

4. 路由协调

主应用和子应用需要协同管理路由:

1
2
3
URL: /order/list
↑ 主应用路由前缀 /order
↑ 子应用内部路由 /list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 主应用注册子应用
registerMicroApps([
{
name: 'order',
entry: '//localhost:3001',
activeRule: '/order', // 匹配 /order 开头的路径
},
]);

// 子应用需要感知自己的基准路径
const baseRoute = window.__POWERED_BY_QIANKUN__
? '/order' // 微前端模式下
: '/'; // 独立运行时

const router = createRouter({
history: createWebHistory(baseRoute),
routes,
});

路由事件协调:子应用内部跳转时,主应用的侧边栏/面包屑需要同步高亮。

四、部署策略

独立部署(推荐)

每个子应用独立构建、独立部署到独立的 URL 路径:

1
2
3
主应用:  https://app.example.com/         → base
订单子应用:https://app.example.com/order/ → 反向代理到 order-app 服务器
用户子应用:https://app.example.com/user/ → 反向代理到 user-app 服务器

Nginx 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 主应用
location / {
proxy_pass http://base-app;
}

# 子应用:反向代理到独立的服务
location /order/ {
proxy_pass http://order-app:3001/;
}

location /user/ {
proxy_pass http://user-app:4001/;
}

统一构建(不推荐)

所有子应用在同一流水线中构建,失去独立部署的优势。

版本管理

1
2
3
4
5
6
7
8
9
10
// 主应用配置子应用版本
{
"microApps": {
"order": {
"dev": "http://localhost:3001/",
"staging": "http://staging-order.example.com/",
"production": "http://prod-order.example.com/"
}
}
}

通过 CI/CD 环境变量注入:

1
2
3
4
5
# .gitlab-ci.yml
deploy:
script:
- sed "s|__ORDER_URL__|$ORDER_URL|g" config.template.json > config.json
- pnpm build

五、微前端 vs 其他方案

维度微前端Monorepo独立多应用
开发隔离高(独立仓库、独立构建)低(同一仓库)
部署耦合高(全量部署)
技术栈任意混用统一任意
代码复用共享模块直接引用
性能按需加载全量打包页面跳转
适用场景大型企业应用中大型项目,技术栈统一完全无关的系统

六、常见问题

Q1: 微前端是否适合所有项目

不适合。如果你的项目:

  • 团队只有 1-3 人
  • 代码量小于 10 万行
  • 技术栈统一且没有遗留系统迁移需求
  • 构建时间在 1 分钟以内

那么微前端带来的复杂度远超收益。

Q2: 样式冲突怎么排查

优先使用 CSS Modules / scoped styles 从子应用源头避免冲突。如果仍出现冲突:

1
2
3
/* 使用浏览器 DevTools 确认冲突规则来自哪个文件 */
/* 在 qiankun/micro-app 中开启样式隔离 */
start({ sandbox: { experimentalStyleIsolation: true } });

Q3: 子应用之间的公共依赖怎么处理

  • Module Federation:通过 shared 配置公共依赖,只加载一次
  • qiankun:使用 externals 将 React/Vue 抽离为全局依赖,子应用不打包框架
  • 独立加载:每个子应用各自打包框架,CDN 缓存命中
1
2
<!-- 主应用 HTML 中预加载公共库 -->
<script src="https://cdn.example.com/react@18.2.0.production.min.js"></script>

Q4: 如何保证子应用之间不互相影响

  • JS:使用沙箱隔离(Proxy / iframe)
  • CSS:使用样式隔离(prefix / Shadow DOM)
  • DOM:子应用只操作自己的容器节点
  • 全局事件:统一管理,子应用卸载时清理 EventListener

Q5: 子应用公共入口加载失败怎么办

1
2
3
4
// Module Federation 远程加载失败时的降级处理
const OrderList = React.lazy(() =>
import('reactApp/OrderList').catch(() => import('./fallback/OrderList'))
);

七、推荐学习路径

  1. 先理解微前端的核心问题和主流方案的取舍(本文)
  2. 动手搭建一个 Module Federation 的 demo:一个 React 主应用 + 一个 Vue 子应用
  3. 尝试 qiankun 的官方示例,理解生命周期和沙箱原理
  4. 阅读 micro-app 或 wujie 源码中样式隔离和 JS 沙箱的实现
  5. 在真实项目中实践,关注性能指标(首屏加载时间、子应用切换耗时)
  6. 研究大厂案例:阿里 qiankun、腾讯 wujie、字节微前端方案