微前端技术选型-带你认识微前端架构
你是不是也遇到过这种情况:项目刚开始时,代码清爽,构建飞快,改个 Bug 几秒钟就能看到效果。但随着业务不断迭代,项目越来越大,构建时间从 30 秒变成 5 分钟,再变成 10 分钟。改一行代码,等半天才能看到效果,开发体验直线下降。
更要命的是,团队越来越大,不同团队用不同的技术栈:老项目用 Vue 2,新项目想用 React,结果只能硬着头皮继续用 Vue 2,技术债越积越多。每次发版都是惊心动魄,生怕一个小改动影响了其他模块,导致线上故障。
这时候,你可能听说过"微前端"这个概念。但市面上方案这么多:qiankun、micro-app、Module Federation……到底该选哪个?每个方案都说自己好,但真正用起来坑在哪里?
这篇文章不讲虚的,只讲实战。我会从真实项目经验出发,深度对比这三种主流微前端方案,告诉你:
- 它们的核心原理是什么
- 各自的优缺点在哪里
- 什么场景适合用哪个方案
- 如何做出正确的技术选型决策
如果你正在考虑引入微前端,这篇文章能帮你少踩很多坑。
微前端发展历程
微前端从 2016 年发展至今,经历了三个阶段。了解这个历程,能帮你理解为什么会有这么多方案。
single-spa
能不能做] --> B[2019
qiankun
怎么做更简单] B --> C[2021
micro-app
更轻量] C --> D[2022
Module Federation
性能更好] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#f0f9ff style D fill:#e8f5e9
运行时集成(qiankun、micro-app):
- 用户打开网页后,浏览器再去下载子应用的代码
- 类比:就像你去餐厅点菜,厨师现做现卖(等待时间长)
- 特点:灵活,但性能有开销
构建时集成(Module Federation):
- 打包时就把代码合并好,用户打开网页时直接用
- 类比:就像你买盒饭,提前做好了,拿到就能吃(快)
- 特点:性能好,开发体验好
核心趋势:从"运行时集成"向"构建时集成"演进,性能和开发体验越来越好。
1. 为什么需要微前端
1.1 单体应用的三大痛点
先说说为什么会有微前端这个东西。如果单体应用没问题,谁会没事找事拆分架构?
真实场景:我之前维护过一个金融交易平台,刚开始只有交易下单、资产查询几个模块,Webpack 构建只要 30 秒。两年后,加了资管、风控、清算、做市等十几个模块,构建时间飙到 12 分钟。
为什么会这样?
- 代码量从 5 万行涨到 50 万行
- 依赖包从 50 个涨到 200 个
- Webpack 要处理的文件越来越多
影响:
- 开发时热更新慢,改一行代码等半天
- CI/CD 流水线变慢,发版时间从 5 分钟变成 20 分钟
- 开发体验极差,工程师怨声载道
真实场景:有一次我们改了一个资管模块的小 Bug,结果发版后交易模块出问题了。原因是两个模块共用了一个工具函数,改动影响了交易模块的逻辑。
为什么会这样?
- 所有代码打包在一起,牵一发动全身
- 改一个模块,整个应用都要重新构建、测试、发版
- 回滚成本高,一个模块出问题,整个应用都要回滚
影响:
- 发版变得小心翼翼,不敢频繁迭代
- 测试成本高,每次都要全量回归测试
- 线上故障风险大
真实场景:我们的较早的项目用的是 Vue 2,团队想用 Vue 3 或 React 重构新模块,但因为是单体应用,要么全部重构(成本太高),要么继续用 Vue 2(技术债越积越多)。
为什么会这样?
- 单体应用只能用一个技术栈
- 技术升级要么全量升级,要么不升级
- 无法尝试新技术,团队技术栈老化
影响:
- 招人难,新人不愿意用老技术
- 技术债越积越多,维护成本越来越高
- 团队技术能力停滞不前
1.2 微前端如何解决这些问题
微前端的核心思想很简单:把一个大应用拆成多个小应用,每个小应用独立开发、独立部署、独立运行。
1. 独立开发
- 每个子应用有自己的代码仓库
- 不同团队维护不同子应用,互不干扰
- 构建时间大幅缩短(只构建自己的子应用)
2. 独立部署
- 改一个子应用,只需要发布这个子应用
- 其他子应用不受影响,降低发版风险
- 可以灰度发布、快速回滚
3. 技术栈无关
- 主应用用 Vue,子应用可以用 React
- 老模块继续用 Vue 2,新模块用 Vue 3
- 可以逐步迁移技术栈,不用一次性重构
举个例子:
还是那个金融交易平台,拆成微前端后:
- 主应用:负责导航、权限、布局(React)
- 交易系统子应用:独立仓库,独立部署(React)
- 资管系统子应用:独立仓库,独立部署(Vue 3)
- 开户系统子应用:独立仓库,独立部署(Vue 2)
- 做市系统子应用:独立仓库,独立部署(React)
- 风控系统子应用:独立仓库,独立部署(React)
效果:
- 构建时间:从 12 分钟降到 2 分钟(只构建改动的子应用)
- 发版风险:改资管模块,只发布资管子应用,不影响交易
- 技术栈:新模块用 Vue 3/React,老模块继续用 Vue 2,逐步迁移
1.2.1 有同事问:为什么不用 Monorepo?
有次在分享会上讲完这些后,有同事问:"既然项目大,为什么不直接用 Monorepo 架构呢?Monorepo 也能把代码拆分成多个包,也能独立开发,为什么还要搞微前端?"
这是个好问题!很多人容易把 Monorepo 和微前端混淆。让我们先分别理解它们是什么,再看它们的区别。
一、Monorepo 是什么?
定义:Monorepo(Monolithic Repository)是一种代码管理策略,把多个项目的代码放在一个 Git 仓库里统一管理。
解决什么问题:
- 多项目依赖管理:A项目依赖B项目,版本同步很麻烦
- 代码共享困难:公共组件、工具函数要复制粘贴
- 工具链不统一:每个项目都要配置一遍 ESLint、Prettier
典型工具:Yarn Workspaces、pnpm workspace
推荐做法:
- 统一依赖版本管理(所有项目用同一个 package.json)
- 共享构建配置(统一的 Webpack、Vite 配置)
- 增量构建(只构建改动的包,提高效率)
目的:提高开发效率,减少重复配置,方便代码共享。
二、微前端是什么?
定义:微前端(Micro Frontends)是一种应用架构模式,把一个大型前端应用拆分成多个独立的子应用,每个子应用可以独立开发、独立部署、独立运行。
解决什么问题:
- 大型应用构建慢:代码量大,构建时间长
- 发版风险高:改一个模块,整个应用都要重新部署
- 技术栈被锁死:老项目用 Vue 2,新项目想用 React,但改不了
- 团队协作困难:多个团队维护同一个代码库,容易冲突
典型方案:
- 微前端框架:qiankun(阿里,基于 single-spa)、micro-app(京东,基于 Web Components)
- 构建工具方案:Module Federation(Webpack 5/Rspack/Vite)
- 传统方案:iframe(隔离性好但有性能和体验问题)
推荐做法:
- 独立构建:每个子应用独立打包,生成独立的 JS 文件
- 独立部署:每个子应用部署到不同的地址
- 运行时集成:主应用在浏览器里动态加载子应用
- 隔离机制:JS 沙箱、样式隔离,避免子应用互相影响
目的:降低发版风险,支持技术栈多样性,提高团队协作效率。
三、分阶段对比
Monorepo 和微前端不是同一层面的概念,它们在不同阶段发挥作用:
| 阶段 | Monorepo | 微前端 |
|---|---|---|
| 开发阶段 | 统一管理代码,方便共享依赖和工具配置 | 每个子应用独立开发,互不干扰 |
| 构建阶段 | 增量构建,只构建改动的包 | 每个子应用独立构建,生成独立的 JS 文件 |
| 部署阶段 | 不影响部署方式(可以是单体部署,也可以是独立部署) | 每个子应用独立部署到不同的地址 |
| 运行时阶段 | 不影响运行方式(用户感知不到 Monorepo) | 主应用动态加载子应用,按需加载 |
| 用户感受 | 用户无感知(Monorepo 只是开发者的工具) | 首屏加载快(按需加载),模块更新不影响其他模块 |
四、它们的关系
核心原因:Monorepo 解决不了独立部署和发版风险的问题。
具体来说:
- Monorepo 只是把代码放在一个仓库里,但不改变部署方式
- 如果你的应用是单体架构,用了 Monorepo 后,还是要整体部署
- 改一个模块,整个应用都要重新构建、测试、发版
- 回滚某个模块,需要回滚整个仓库或手动 cherry-pick
所以:如果你的痛点是"发版风险高、想独立部署",只用 Monorepo 是解决不了的,必须用微前端。
代码仓库结构:
finance-platform/ (Monorepo 仓库)
├── apps/
│ ├── shell/ (主应用)
│ ├── trading/ (交易子应用)
│ ├── asset/ (资管子应用)
│ └── risk/ (风控子应用)
├── packages/
│ ├── shared-components/ (共享组件库)
│ └── shared-utils/ (共享工具函数)
└── package.json (统一依赖管理)
构建流程:
- 每个子应用独立构建:
pnpm build --filter=trading - 生成独立的产物:
trading/dist/index.js - 共享的组件和工具函数可以直接引用,不用复制粘贴
部署流程:
- 每个子应用独立部署到不同的地址:
https://finance.com/apps/trading/https://finance.com/apps/asset/https://finance.com/apps/risk/
- 改资管模块 → 只重新构建 asset → 只部署
/apps/asset/
效果:
- 代码统一管理(Monorepo 的优势)
- 共享组件和工具函数(Monorepo 的优势)
- 独立构建、独立部署(微前端的优势)
- 降低发版风险(微前端的优势)
- 只想统一管理代码、共享依赖?→ 用 Monorepo
- 需要独立部署、降低发版风险?→ 用 微前端
- 两者都需要?→ Monorepo + 微前端
五、各自的局限
1. 回滚问题:
- 场景:资管模块上线后发现Bug,想回滚到上个版本
- 问题:但这时交易模块已经有新提交了,怎么办?
- 方案1:回滚整个仓库 → 交易模块也被回滚了(影响其他模块)
- 方案2:手动 cherry-pick → 工作量大,容易出错
2. 仓库膨胀:
- 随着项目增多,仓库越来越大,clone 和 pull 都很慢
- Git 操作变慢,影响开发体验
3. 权限管理困难:
- 所有人都能看到所有代码,无法做细粒度的权限控制
1. 复杂度增加:
- 需要处理子应用通信、路由管理、依赖共享等问题
- 学习成本和维护成本增加
2. 性能开销:
- 加载多个子应用,网络请求增加
- JS 沙箱、样式隔离有一定的性能开销
3. 调试困难:
- 多个子应用同时运行,调试和排查问题更复杂
1.3 微前端适用场景
微前端不是银弹,不是所有项目都适合。
1. 大型项目
- 代码量 > 10 万行
- 模块 > 10 个
- 构建时间 > 5 分钟
2. 多团队协作
- 团队 > 3 个
- 不同团队维护不同模块
- 需要独立开发、独立部署
3. 技术栈迁移
- 老项目用 Vue 2,想迁移到 Vue 3
- 想尝试新技术(React、Svelte)
- 不想一次性重构
4. 高频迭代
- 需要频繁发版
- 不同模块发版节奏不同
- 需要灰度发布、快速回滚
1. 小型项目
- 代码量 < 5 万行
- 模块 < 5 个
- 单人或小团队维护
原因:微前端有额外的复杂度(子应用通信、路由管理、依赖共享),小项目用微前端是杀鸡用牛刀。
2. 性能要求极高的项目
- 首屏加载时间要求 < 1 秒
- 对运行时性能要求极高
原因:微前端会增加一些性能开销(加载多个子应用、JS 沙箱、样式隔离),如果性能要求极高,需要慎重评估。
3. 团队技术能力不足
- 团队对前端工程化不熟悉
- 没有人能 hold 住微前端架构
原因:微前端有一定的学习成本和维护成本,如果团队技术能力不足,可能会适得其反。
1.4 微前端架构图
2. QianKun框架
2.1 qiankun 是什么?
定义:qiankun 是阿里开源的微前端框架,基于 single-spa 封装,提供开箱即用的微前端解决方案。
核心特点:
- 技术栈无关:主应用和子应用可以使用不同的技术栈(React、Vue、Angular 等)
- JS 沙箱:自动隔离子应用的全局变量,避免污染
- 样式隔离:支持 Shadow DOM 和 scoped CSS,避免样式冲突
- 开箱即用:无需复杂配置,几行代码即可接入
- 生态成熟:社区活跃,文档完善,大厂背书
2.2 qiankun 核心概念
1. 主应用(基座应用)
- 负责注册和加载子应用
- 管理全局路由和权限
- 提供全局状态和通信机制
2. 子应用(微应用)
- 独立开发、独立部署
- 导出生命周期函数(bootstrap、mount、unmount)
- 可以是任何技术栈
3. 生命周期
bootstrap:子应用初始化(只执行一次)mount:子应用挂载(每次激活时执行)unmount:子应用卸载(每次切换时执行)
4. 工作原理
qiankun 加载子应用的流程如下:
从流程图可以看出,qiankun 是在浏览器运行时动态加载子应用的全部资源,这就是为什么它被称为「运行时集成」方案。
2.3 快速上手
主应用配置
// main.js
import { registerMicroApps, start } from 'qiankun';
// 注册子应用
registerMicroApps([
{
name: 'trading-system', // 子应用名称
entry: '//localhost:3001', // 子应用地址
container: '#subapp-container', // 子应用挂载的容器
activeRule: '/trading', // 激活路由
},
{
name: 'asset-system',
entry: '//localhost:3002',
container: '#subapp-container',
activeRule: '/asset',
},
]);
// 启动 qiankun
start();
子应用配置(以 React 为例)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
let root = null;
// 导出生命周期函数
export async function bootstrap() {
console.log('交易系统初始化');
}
export async function mount(props) {
console.log('交易系统挂载', props);
const container = props.container || document.querySelector('#root');
root = ReactDOM.createRoot(container);
root.render(<App />);
}
export async function unmount(props) {
console.log('交易系统卸载');
root?.unmount();
}
// 独立运行时的逻辑
if (!window.__POWERED_BY_QIANKUN__) {
mount({});
}
子应用 Webpack 配置
// webpack.config.js
module.exports = {
output: {
library: 'tradingSystem', // 子应用名称
libraryTarget: 'umd', // 必须是 umd 格式
publicPath: '//localhost:3001/', // 子应用地址
},
devServer: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*', // 允许跨域
},
},
};
2.4 实战:金融平台微前端改造
下面演示如何用 qiankun 把交易系统(React)和资管系统(Vue 3)集成到主应用中,实现独立开发部署、共享登录状态。
主应用配置(React)
// src/App.jsx
import { registerMicroApps, start, initGlobalState } from 'qiankun';
import { useEffect } from 'react';
function App() {
useEffect(() => {
// 初始化全局状态(用于应用间通信)
// user 和 token 从登录接口获取
const actions = initGlobalState({
user: { id: 1001, name: '张三', role: 'trader' },
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
});
// 注册子应用
registerMicroApps([
{
name: 'trading-system',
entry: '//localhost:3001',
container: '#subapp-container',
activeRule: '/trading',
props: { actions }, // 传递全局状态
},
{
name: 'asset-system',
entry: '//localhost:3002',
container: '#subapp-container',
activeRule: '/asset',
props: { actions },
},
]);
// 启动 qiankun
start({
sandbox: { strictStyleIsolation: true }, // 开启严格样式隔离
});
}, []);
return (
<div className="main-app">
<nav>
<a href="/trading">交易系统</a>
<a href="/asset">资管系统</a>
</nav>
<div id="subapp-container"></div>
</div>
);
}
export default App;
子应用改造(交易系统 - React)
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
let root = null;
export async function bootstrap() {
console.log('[交易系统] 初始化');
}
export async function mount(props) {
console.log('[交易系统] 挂载', props);
// 接收主应用传递的全局状态
props.actions.onGlobalStateChange((state, prev) => {
console.log('[交易系统] 全局状态变化', state, prev);
});
// 获取用户信息
const { user, token } = props.actions.getGlobalState();
console.log('[交易系统] 用户信息', user, token);
// 渲染应用
const container = props.container || document.querySelector('#root');
root = ReactDOM.createRoot(container);
root.render(<App user={user} token={token} />);
}
export async function unmount(props) {
console.log('[交易系统] 卸载');
root?.unmount();
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
mount({
actions: {
onGlobalStateChange: () => {},
getGlobalState: () => ({ user: null, token: null }),
},
});
}
子应用改造(资管系统 - Vue 3)
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
let app = null;
export async function bootstrap() {
console.log('[资管系统] 初始化');
}
export async function mount(props) {
console.log('[资管系统] 挂载', props);
// 接收主应用传递的全局状态
props.actions.onGlobalStateChange((state, prev) => {
console.log('[资管系统] 全局状态变化', state, prev);
});
// 获取用户信息
const { user, token } = props.actions.getGlobalState();
// 渲染应用
const container = props.container || document.querySelector('#app');
app = createApp(App);
app.provide('user', user);
app.provide('token', token);
app.mount(container);
}
export async function unmount(props) {
console.log('[资管系统] 卸载');
app?.unmount();
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
mount({
actions: {
onGlobalStateChange: () => {},
getGlobalState: () => ({ user: null, token: null }),
},
});
}
运行效果
当用户访问 /trading 路由时,最终的 HTML 结构如下:
<div class="main-app">
<nav>
<a href="/trading">交易系统</a>
<a href="/asset">资管系统</a>
</nav>
<div id="subapp-container">
<!-- qiankun 会把交易系统直接挂载到这个容器中 -->
<!-- 交易系统的 React 组件渲染内容 -->
</div>
</div>
从上面的代码可以看出,qiankun 的接入流程很清晰:主应用通过 registerMicroApps 注册子应用,子应用导出 bootstrap/mount/unmount 三个生命周期函数,通过 initGlobalState 实现应用间通信。那 qiankun 用起来到底怎么样?我们来看看它的优缺点。
2.5 qiankun 的优缺点
1. 开箱即用
- 无需复杂配置,几行代码即可接入
- 自动处理 JS 沙箱和样式隔离
- 提供完整的生命周期管理
2. 技术栈无关
- 主应用和子应用可以使用不同的技术栈
- 支持 React、Vue、Angular、jQuery 等
- 老项目可以平滑迁移
3. 生态成熟
- 阿里大厂背书,社区活跃
- 文档完善,示例丰富
- 有大量实战案例可参考
4. 功能完善
- 支持预加载、预获取
- 支持应用间通信(全局状态)
- 支持多种沙箱模式
1. 在浏览器里加载子应用,性能有开销
- qiankun 是在用户打开网页后,浏览器再去下载子应用的代码,不是在打包时就把代码合并好
- 类比:就像你点外卖,qiankun 是"现做现送"(用户等待时间长),而构建时集成是"提前做好放保温箱"(用户拿到就能吃)
- JS 沙箱和样式隔离需要额外的运行时代码,有性能开销
- 子应用切换时需要重新挂载和卸载,切换有延迟(通常 500ms-1s)
- 首次加载子应用时,需要下载 JS、解析、执行,用户会感知到加载过程
2. 需要改造项目,学习成本高
- 核心问题:现有项目不能直接接入,必须改造代码
- 需要导出生命周期函数(bootstrap、mount、unmount)
- 需要修改 Webpack 配置(library、libraryTarget、publicPath)
- 需要理解 qiankun 的沙箱机制和生命周期
- 团队需要时间学习和适应
3. 没有类型安全,开发体验一般
- 核心问题:子应用是运行时加载的,TypeScript 无法提供类型检查
- 主应用调用子应用的方法时,没有类型提示和自动补全
- IDE 无法跳转到子应用的代码
- 重构时容易遗漏,只能在运行时发现问题
适合用 qiankun 的场景:
- 大型项目,需要拆分成多个子应用
- 多个团队协作,技术栈不统一
- 老项目需要平滑迁移到新技术栈
- 需要独立部署和发版
- 团队有一定的微前端经验
不适合用 qiankun 的场景:
- 小型项目,代码量不大
- 团队技术能力不足,无法 hold 住微前端
- 对性能要求极高的场景
- 需要极强的应用间通信(qiankun 的通信机制相对简单)
三、micro-app:能不能更简单一点?
用了一段时间 qiankun 之后,你可能会有这样的感受:
- 子应用必须导出三个生命周期函数,改造成本不低
- Webpack 配置要改成 UMD 格式,新手容易踩坑
- 主应用注册子应用的代码有点啰嗦
这时候你可能会想:有没有更简单的方案?
京东的 micro-app 就是冲着这个痛点来的。它的核心理念是:像使用 iframe 一样简单,但没有 iframe 的各种问题。
3.1 micro-app 的核心思路
micro-app 基于 Web Components 的思想,把子应用封装成一个自定义 HTML 标签:
<!-- 就像用 iframe 一样,但不是 iframe -->
<micro-app name="trading" url="http://localhost:3001"></micro-app>
对比一下 qiankun 的写法:
// qiankun 需要用 JS 注册子应用
registerMicroApps([
{
name: 'trading',
entry: '//localhost:3001',
container: '#container',
activeRule: '/trading',
},
]);
start();
哪个更直观?一目了然。
3.2 快速上手
安装
npm install @micro-zoe/micro-app
主应用配置(React)
// src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import microApp from '@micro-zoe/micro-app';
import App from './App';
// 初始化 micro-app
microApp.start();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
就这么简单,主应用只需要调用 microApp.start() 初始化一下。
主应用使用子应用
// src/App.jsx
import { useEffect } from 'react';
function App() {
return (
<div className="main-app">
<nav>
<a href="/trading">交易系统</a>
<a href="/asset">资管系统</a>
</nav>
{/* 直接用标签嵌入子应用,就像用 img 标签一样自然 */}
<micro-app
name="trading"
url="http://localhost:3001"
baseroute="/trading"
></micro-app>
</div>
);
}
export default App;
name:子应用的唯一标识,不能重复url:子应用的访问地址baseroute:子应用的基础路由,用于路由匹配
子应用改造
这是 micro-app 最爽的地方:子应用几乎不用改代码。
只需要做两件事:
1. 设置跨域(开发环境)
// vite.config.js(子应用)
export default defineConfig({
server: {
port: 3001,
cors: true, // 允许跨域
origin: 'http://localhost:3001', // 保证静态资源路径正确
},
});
2. 处理路由基础路径(可选)
// src/router.js(子应用)
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
// 如果是被 micro-app 加载,使用主应用传递的 baseroute
history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || '/'),
routes: [...],
});
没了。真的没了。
对比一下 qiankun 子应用要做的事:
- 导出 bootstrap、mount、unmount 三个生命周期函数
- 修改 Webpack 配置(library、libraryTarget、publicPath)
- 处理独立运行和被加载两种模式
- 配置跨域
micro-app 把这些复杂度都藏在了框架内部,子应用基本上可以当普通项目来开发。
3.3 实战:金融平台微前端改造
还是用交易系统(React)和资管系统(Vue 3)的例子,看看用 micro-app 怎么做。
主应用配置(React)
// src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import microApp from '@micro-zoe/micro-app';
import App from './App';
microApp.start({
// 全局配置
'disable-sandbox': false, // 开启沙箱(默认开启)
'disable-scopecss': false, // 开启样式隔离(默认开启)
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
主应用路由和子应用嵌入
// src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
// 交易系统页面
function TradingPage() {
return (
<micro-app
name="trading"
url="http://localhost:3001"
baseroute="/trading"
// 向子应用传递数据
data={{ user: { id: 1001, name: '张三' }, token: 'xxx' }}
></micro-app>
);
}
// 资管系统页面
function AssetPage() {
return (
<micro-app
name="asset"
url="http://localhost:3002"
baseroute="/asset"
data={{ user: { id: 1001, name: '张三' }, token: 'xxx' }}
></micro-app>
);
}
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/trading">交易系统</Link>
<Link to="/asset">资管系统</Link>
</nav>
<Routes>
<Route path="/trading/*" element={<TradingPage />} />
<Route path="/asset/*" element={<AssetPage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
子应用接收数据(交易系统 - React)
// src/App.jsx(交易系统)
import { useEffect, useState } from 'react';
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
// 获取主应用传递的数据
const data = window.microApp?.getData();
if (data) {
setUser(data.user);
console.log('收到主应用数据', data);
}
// 监听主应用数据变化
const dataListener = (data) => {
console.log('主应用数据更新', data);
setUser(data.user);
};
window.microApp?.addDataListener(dataListener);
return () => {
window.microApp?.removeDataListener(dataListener);
};
}, []);
return (
<div>
<h1>交易系统</h1>
{user && <p>当前用户:{user.name}</p>}
</div>
);
}
export default App;
子应用接收数据(资管系统 - Vue 3)
<!-- src/App.vue(资管系统) -->
<template>
<div>
<h1>资管系统</h1>
<p v-if="user">当前用户:{{ user.name }}</p>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const user = ref(null);
onMounted(() => {
// 获取主应用传递的数据
const data = window.microApp?.getData();
if (data) {
user.value = data.user;
console.log('收到主应用数据', data);
}
// 监听主应用数据变化
window.microApp?.addDataListener(dataListener);
});
function dataListener(data) {
console.log('主应用数据更新', data);
user.value = data.user;
}
onUnmounted(() => {
window.microApp?.removeDataListener(dataListener);
});
</script>
运行效果
当用户访问 /trading 路由时,最终的 HTML 结构如下:
<div class="main-app">
<nav>
<a href="/trading">交易系统</a>
<a href="/asset">资管系统</a>
</nav>
<!-- micro-app 是一个自定义元素 -->
<micro-app name="trading" url="http://localhost:3001">
<!-- 子应用的内容被渲染在这里 -->
<!-- micro-app 默认通过 CSS 前缀实现样式隔离,不是真正的 Shadow DOM -->
<div data-micro-app="trading">
<h1>交易系统</h1>
<p>当前用户:张三</p>
</div>
</micro-app>
</div>
从代码量和改造成本来看,micro-app 确实比 qiankun 简单不少。主应用只需要用标签嵌入,子应用几乎不用改。
3.4 micro-app 的优缺点
1. 接入成本极低
- 主应用:一行
microApp.start(),然后用标签嵌入子应用 - 子应用:几乎不用改代码,配置跨域就行
- 不需要改 Webpack 配置,不需要导出生命周期函数
2. 使用方式直观
- 像使用 HTML 标签一样使用子应用
- 通过 data 属性传递数据,通过事件接收消息
- 学习成本低,团队容易上手
3. 样式隔离更彻底
- 默认通过 CSS 前缀实现样式隔离,也支持开启 Shadow DOM
- 配置简单,不需要额外的沙箱代码
4. 技术栈无关
- 和 qiankun 一样,支持任意技术栈
- React、Vue、Angular、jQuery 都可以
1. 本质上还是运行时加载
- 和 qiankun 一样,子应用是在浏览器里动态加载的
- 首次加载子应用有延迟,用户能感知到加载过程
- 运行时需要额外的沙箱和样式隔离代码
2. 还是没有类型安全
- 主应用和子应用之间的数据传递没有类型检查
- IDE 无法跳转到子应用的代码
- 重构时容易遗漏,只能在运行时发现问题
3. 样式隔离有局限
- 默认的 CSS 前缀隔离方案不是 100% 隔离
- 如果开启 Shadow DOM,某些第三方库可能不兼容
- 弹窗、下拉菜单等组件可能需要特殊处理
4. 社区相对较小
- 比 qiankun 出来得晚,社区生态不如 qiankun
- 遇到问题可能不太容易找到解决方案
适合用 micro-app 的场景:
- 想快速接入微前端,不想改太多代码
- 团队微前端经验不多,想降低学习成本
- 对接入成本比较敏感的项目
不适合用 micro-app 的场景:
- 对性能要求极高(运行时加载有开销)
- 需要强类型安全和 IDE 支持
- 使用了大量不兼容 Shadow DOM 的第三方库
3.5 qiankun vs micro-app:怎么选?
说了这么多,qiankun 和 micro-app 到底选哪个?简单总结一下:
选 qiankun:团队有微前端经验,愿意花时间改造项目,需要成熟稳定的方案。
选 micro-app:想快速接入,不想改太多代码,团队微前端经验不多。
共同的问题:都是运行时加载,性能有开销,没有类型安全。
那有没有一种方案,既能享受微前端的好处,又能解决运行时加载和类型安全的问题?
有的。下一部分我们来聊聊 Module Federation。
四、Module Federation:终于等到你
4.1 用了 qiankun/micro-app 之后的困惑
我之前用 qiankun 搭建了金融平台的微前端架构,5 个子应用(交易、资管、开户、做市、风控)跑起来了,但用了一段时间后,我发现了一些让我很难受的问题。
先说明一下:qiankun 和 micro-app 是两个独立的微前端框架,但它们的设计思路相似——都是「运行时加载整个子应用」。下面说的问题,两个框架都存在。
问题一:加载太重,React 加载了好几份
我们 5 个子应用里有 3 个用 React(交易、做市、风控),2 个用 Vue(资管用 Vue 3、开户用 Vue 2)。每个子应用打包后都包含一份完整的框架代码。用户访问首页,加载主应用 + 交易系统,光 React 就加载了 2 份。
这不是 bug,这是 qiankun/micro-app 的设计决定的——它们把子应用当成「独立网页」来加载,自然每个子应用都要带上自己的依赖。
- React + ReactDOM 压缩后约 140KB
- 主应用 + 3 个 React 子应用 = 4 份 React = 560KB
- Vue 3 约 90KB,Vue 2 约 80KB
- 如果能共享,React 只需要 140KB
- 浪费了 420KB+ 带宽和加载时间
问题二:只想用一个组件,却要加载整个子应用
有个需求:在首页 Dashboard 上展示交易系统的「快捷下单」组件和风控系统的「风险预警」组件。
用 qiankun 怎么做?
// qiankun:手动加载子应用到指定容器
loadMicroApp({
name: 'trading',
entry: 'http://localhost:3001',
container: '#trading-container', // 只想要一个组件,却要加载整个交易系统
});
loadMicroApp({
name: 'risk',
entry: 'http://localhost:3005',
container: '#risk-container', // 只想要一个预警组件,却要加载整个风控系统
});
用 micro-app 怎么做?
<!-- micro-app:用标签加载 -->
<micro-app name="trading" url="http://localhost:3001" />
<micro-app name="risk" url="http://localhost:3005" />
两个小组件,加载了 2 个完整应用,总共 4MB+ 的 JS。这合理吗?
问题三:子应用强依赖主应用
qiankun/micro-app 的子应用必须通过主应用的「坑位」才能渲染。如果我想让交易系统独立访问,或者让资管系统引用交易系统的组件,做不到。
问题四:没有类型安全
主应用传给子应用的数据没有类型检查。改了 user 对象的字段名,TypeScript 不会报错,只能在运行时发现问题。
4.2 qiankun/micro-app 的加载流程
先看看 qiankun/micro-app 是怎么加载子应用的:
问题很明显:每次都要加载完整的子应用 JS,包括重复的 React。
4.3 我理想中的微前端
如果有一种方案,能做到这些就好了:
- 按需加载:我只想用「快捷下单」组件,就只加载这个组件的代码
- 共享依赖:React 只加载一份,5 个子应用共用
- 独立运行:子应用可以独立跑,也可以被其他应用引用
- 类型安全:import 的时候有类型提示,重构不怕漏改
这就是 Module Federation 的设计理念。
4.4 Module Federation 是什么
Module Federation(模块联邦)是 Webpack 5 引入的功能,Vite 通过 @originjs/vite-plugin-federation 插件也支持了。本文的示例都基于 Vite,如果你用 Webpack 5,配置方式略有不同但原理一样。
它的核心思想是:
让不同的应用可以共享模块,就像 npm 包一样,你可以 import 远程应用暴露的组件。
看看 Module Federation 的加载流程:
对比一下:
- qiankun/micro-app:加载整个子应用 800KB+(包含重复的 React)
- Module Federation:只加载需要的组件 25KB(复用已有的 React)
4.5 开发阶段:5 个子应用、10 个前端怎么协作
我们团队有 10 个前端开发,维护 5 个子应用。来看看不同方案的开发体验:
常规方案(npm 包共享)
- 改了公共组件 → 发布 npm 包 → 每个项目升级版本 → 重新构建
- 调试痛苦:改一行代码,要等 npm 发布,其他项目才能看到效果
qiankun/micro-app
- 子应用独立开发,但调试时必须启动主应用
- 想看效果?启动主应用 + 子应用,至少 2 个终端
Module Federation
- 改了交易系统的组件 → 资管系统实时看到效果(热更新)
- 不需要发 npm 包,不需要重新构建
- 每个应用可以独立运行,也可以引用其他应用的模块
Module Federation 的开发体验就像在一个 Monorepo 里开发,但代码可以分布在不同的仓库。改了组件,其他应用实时生效,不需要发包、不需要重启。
4.6 实战:金融平台微前端改造
现在用 Module Federation 重构我们的金融平台,5 个子应用:
- 交易系统(React):暴露「快捷下单」「持仓列表」组件
- 资管系统(Vue 3):暴露「净值曲线」「持仓分析」组件
- 开户系统(Vue 2):暴露「开户表单」组件
- 做市系统(React):暴露「报价面板」组件
- 风控系统(React):暴露「风险预警」「风控报表」组件
交易系统配置(暴露组件)
// trading-system/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'trading',
filename: 'remoteEntry.js',
// 暴露组件给其他应用使用
exposes: {
'./QuickOrder': './src/components/QuickOrder.jsx',
'./PositionList': './src/components/PositionList.jsx',
},
// 共享依赖,避免重复加载
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
build: {
target: 'esnext',
},
});
风控系统配置(暴露组件)
// risk-system/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'risk',
filename: 'remoteEntry.js',
exposes: {
'./RiskAlert': './src/components/RiskAlert.jsx',
'./RiskReport': './src/components/RiskReport.jsx',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
build: {
target: 'esnext',
},
});
主应用配置(引用远程组件)
// main-app/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'main',
// 声明远程应用
remotes: {
trading: {
external: 'http://localhost:3001/assets/remoteEntry.js',
format: 'esm',
},
risk: {
external: 'http://localhost:3005/assets/remoteEntry.js',
format: 'esm',
},
asset: {
external: 'http://localhost:3002/assets/remoteEntry.js',
format: 'esm',
},
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
});
主应用使用远程组件
// main-app/src/pages/Dashboard.jsx
import { lazy, Suspense } from 'react';
// 动态导入远程组件,就像导入本地组件一样
const QuickOrder = lazy(() => import('trading/QuickOrder'));
const RiskAlert = lazy(() => import('risk/RiskAlert'));
const NetValueChart = lazy(() => import('asset/NetValueChart'));
function Dashboard() {
return (
<div className="dashboard">
<h1>金融平台 Dashboard</h1>
<div className="grid">
{/* 交易系统的快捷下单组件 */}
<Suspense fallback={<div>加载中...</div>}>
<QuickOrder />
</Suspense>
{/* 风控系统的风险预警组件 */}
<Suspense fallback={<div>加载中...</div>}>
<RiskAlert />
</Suspense>
{/* 资管系统的净值曲线组件 */}
<Suspense fallback={<div>加载中...</div>}>
<NetValueChart />
</Suspense>
</div>
</div>
);
}
export default Dashboard;
看到了吗?同一个页面,展示了 3 个不同子应用的组件,每个组件按需加载,React 只有一份。
这是 qiankun/micro-app 做不到的——它们只能加载完整的子应用,不能只加载某个组件。
4.7 部署方案
Module Federation 的部署和普通前端项目一样,每个应用独立构建、独立部署。
Nginx 配置
# nginx.conf
server {
listen 80;
server_name finance.example.com;
# 主应用
location / {
root /www/main-app/dist;
try_files $uri $uri/ /index.html;
}
# 交易系统(远程模块)
location /trading/ {
alias /www/trading-system/dist/;
add_header Access-Control-Allow-Origin *;
}
# 资管系统(远程模块)
location /asset/ {
alias /www/asset-system/dist/;
add_header Access-Control-Allow-Origin *;
}
# 风控系统(远程模块)
location /risk/ {
alias /www/risk-system/dist/;
add_header Access-Control-Allow-Origin *;
}
}
CICD 流程
每个子应用独立部署,更新后其他应用自动获取最新版本,不需要重新构建主应用。
4.8 Module Federation 的优缺点
- 按需加载:只加载需要的模块,不是整个应用
- 共享依赖:React 等公共库只加载一份
- 类型安全:可以共享 TypeScript 类型定义
- 独立部署:每个应用独立构建、独立部署
- 开发体验好:热更新,改了组件其他应用实时生效
- 配置复杂:需要理解 shared、exposes、remotes 等概念
- 版本管理:共享依赖的版本需要协调
- 没有内置 CSS 沙箱:样式可能冲突,需要额外方案:
- CSS Modules(推荐)
- CSS-in-JS(styled-components、emotion)
- Vite 插件(如 vite-plugin-css-prefix-auto,自动给选择器加作用域前缀)
4.9 三种方案对比总结
| 对比项 | qiankun | micro-app | Module Federation |
|---|---|---|---|
| 加载粒度 | 整个应用 | 整个应用 | 单个模块 |
| 依赖共享 | 不支持 | 不支持 | 支持 |
| 类型安全 | 无 | 无 | 有 |
| JS 沙箱 | 有(Proxy) | 有 | 有(模块作用域) |
| CSS 沙箱 | 有 | 有(前缀隔离) | 无(需插件) |
| 接入成本 | 中等 | 低 | 中等 |
| 性能 | 差(加载整个应用) | 差(加载整个应用) | 好(按需加载模块) |
| 适合场景 | 整合独立系统 | 快速接入 | 模块共享、性能优先 |
- 选 qiankun:需要整合多个独立系统,技术栈不统一,需要沙箱隔离
- 选 micro-app:想快速接入,不想改太多代码
- 选 Module Federation:追求性能,需要模块级共享,团队技术栈统一
五、总结
微前端不是银弹,选型要根据实际场景:
- 如果你的项目是多个独立系统拼接,技术栈不统一,选 qiankun
- 如果你想快速接入,不想改太多代码,选 micro-app
- 如果你追求性能和开发体验,团队技术栈统一,选 Module Federation
我个人更推荐 Module Federation,因为它的设计理念更先进——不是把应用「拼」在一起,而是让应用之间可以「共享模块」。如果这还不算是微前端的未来方向,什么才是呢?