前端微前端与模块联邦
微前端是将前端应用拆分为多个独立开发、独立部署、独立运行的小型应用,通过组合方式构成一个完整产品。本文从实战角度出发,系统性讲解微前端概念、方案选型、Module Federation 与 Qiankun 实践,以及工程化要点。
一、微前端核心概念
1.1 什么是微前端
微前端 (Micro Frontends) 是指将前端应用拆分为多个功能独立的子应用,每个子应用可由不同团队使用不同技术栈独立开发、测试、部署,最终在主应用(基座)中统一组合呈现。
┌─────────────────────────────────┐
│ 主应用(基座容器) │
├──────────┬──────────┬───────────┤
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐
│ 导航 │ │ 商品 │ │ 订单 │ │ 用户 │
│ (团队A) │ │ (团队B) │ │ (团队C) │ │ (团队D) │
│ Vue 3 │ │ React │ │ Vue 2 │ │ Angular │
└────────┘ └────────┘ └────────┘ └──────────┘
1.2 为什么需要微前端
| 痛点 | 单体前端表现 | 微前端解决 |
|---|---|---|
| 代码规模爆炸 | 单仓库百万行代码,构建耗时 > 10 分钟 | 子应用独立构建,单应用 < 30 秒 |
| 团队协作冲突 | 多人修改同一仓库,git merge 频繁冲突 | 子应用独立仓库,互不干扰 |
| 技术栈锁定 | 一荣俱荣,一损俱损,升级框架成本极高 | 子应用自主选型,渐进升级 |
| 独立发布困难 | 改一行代码需重新部署整个应用 | 子应用独立部署,灰度发布 |
| 存量系统迁移 | 老系统重构需要一次性替换 | 新功能用新技术栈,逐步替换老系统 |
1.3 与后端微服务的类比
| 维度 | 后端微服务 | 前端微前端 |
|---|---|---|
| 拆分粒度 | 按业务领域拆分服务 | 按业务域/页面拆分子应用 |
| 通信方式 | HTTP/RPC/消息队列 | 自定义事件/共享状态/路由参数 |
| 数据隔离 | 数据库独立 | 样式/JS沙箱/Store 隔离 |
| 部署方式 | 独立部署、独立扩缩容 | 独立构建、独立部署(CDN) |
| 技术异构 | 不同语言(Java/Go/Python) | 不同框架(Vue/React/Angular) |
| 治理手段 | 注册中心/配置中心/网关 | 基座应用/沙箱/路由分发 |
1.4 适用场景判断矩阵
不适用微前端的情况:
- 团队 1~5 人,应用简单(CRUD 后台管理)
- 技术栈统一且短期内不会变更
- 没有独立发布的需求
- 项目周期短、交付时间紧
适用微前端的情况:
| 场景 | 说明 | 决策 |
|---|---|---|
| 多团队协作大型应用 | 5+ 个团队开发同一产品,各自负责独立模块 | ✅ 强烈推荐 |
| 存量系统渐进迁移 | 旧系统 jQuery/Vue2 -> 新系统 Vue3/React | ✅ 推荐使用 |
| 中台/平台型产品 | 多个业务线共享同一个 Portal | ✅ 推荐使用 |
| 第三方插件/扩展体系 | 允许第三方开发独立模块挂载到主应用 | ✅ 必须使用 |
| 简单后台管理系统 | 2~3 人团队维护,1 个 repo | ❌ 不推荐(引入复杂度 > 收益) |
| 独立 H5 活动页 | 一次性营销活动,单页应用 | ❌ 完全不需要 |
二、微前端方案对比
2.1 主流方案速览
| 方案 | 出品方 | 核心原理 | 技术栈适配 | 成熟度 | GitHub Stars |
|---|---|---|---|---|---|
| Qiankun | 阿里 | 基于 single-spa,HTML Entry + Proxy 沙箱 | 任意框架 | 高 | 16k+ |
| Micro-app | 京东 | WebComponent + CustomEvent,类 iframe 思想 | 任意框架 | 高 | 5k+ |
| Wujie | 腾讯 | iframe + WebComponent,WebComponent 做 UI 挂载 | 任意框架 | 中 | 4k+ |
| Module Federation | Webpack 官方 | 运行时模块加载 + 依赖共享 | 需 Webpack 5 / Vite 插件 | 高 | Webpack 官方 |
| single-spa | 社区 | 路由分发 + 生命周期管理 | 任意框架 | 高 | 13k+ |
2.2 方案对比表
| 维度 | Qiankun | Micro-app | Wujie | Module Federation |
|---|---|---|---|---|
| 沙箱机制 | ✅ Proxy 沙箱 + Script 劫持 | ✅ WebComponent + Proxy 沙箱 | ✅ iframe 天然隔离 + Proxy | ❌ 无沙箱(依赖 shared 隔离) |
| 样式隔离 | ✅ StrictStyleIsolation / Scoped CSS | ✅ Shadow DOM / Scoped | ✅ 天然隔离(iframe 天然自带) | ❌ CSS Modules / CSS-in-JS 需自建 |
| 子应用改造成本 | ⚠️ 需改造 webpack 导出生命周期 | ✅ 几乎零改造 | ✅ 几乎零改造 | ✅ 导出 expose 配置即可 |
| 通信机制 | initGlobalState + props | CustomEvent + data | window.parent 通信 | shared 模块 + 自定义事件 |
| 性能开销 | 中(Proxy 沙箱有一定损耗) | 中(WebComponent 额外 DOM) | 低(iframe 复用,DOM 在 Shadow 中) | 最低(纯模块加载) |
| 跨技术栈 | ✅ 支持 | ✅ 支持 | ✅ 支持 | ⚠️ 需 shared 暴露统一接口 |
| SSR 支持 | ❌ 不支持 | ❌ 不支持 | ❌ 不支持 | ✅ 支持 Module Federation SSR |
| 构建工具 | webpack(需插件) | webpack(需插件) | webpack(需插件) | Webpack 5 / Vite(插件) |
| 运行时 | 路由劫持 | 自定义路由 | iframe 桥接 | 模块运行时 |
| 首屏加载 | 慢(需加载 HTML Entry) | 中 | 最快(iframe 预加载) | 快(按需加载 remote 模块) |
| 坑点 | 内存泄漏排查困难;子应用频繁切换加载慢 | Shadow DOM 下某些 UI 库样式异常 | iframe 焦点/键盘事件问题 | 无沙箱,子应用需严格约定 |
2.3 选型建议
graph TD
A[选择微前端方案] --> B{是否需要彻底隔离?}
B -->|是| C{需要跨技术栈?}
B -->|否| D{技术栈统一?}
C -->|是| E[Wujie / Micro-app]
C -->|否| F[Qiankun]
D -->|是| G[Module Federation]
D -->|否| E
| 团队情况 | 推荐方案 | 理由 |
|---|---|---|
| 阿里系技术栈,已有 single-spa 经验 | Qiankun | 生态完善,文档丰富 |
| 需要最小改造成本接入 | Micro-app | 几乎零改造,类 iframe 设计 |
| 极致隔离要求(如第三方插件市场) | Wujie | iframe 天然隔离,安全可靠 |
| 技术栈统一(全 Vue3 或全 React),追求性能 | Module Federation | 零沙箱开销,native 模块加载 |
| 已有 Webpack 5 项目,快速验证 | Module Federation | 零额外依赖,仅配置即可 |
三、Module Federation 深度实践
3.1 原理说明
Module Federation(模块联邦)是 Webpack 5 的核心特性,允许多个独立构建的应用在运行时动态加载彼此模块。
┌─────────────────────┐ 运行时远程加载 ┌─────────────────────┐
│ Host(主应用) │ ◄──────────────── │ Remote(远程应用) │
│ ┌────────────────┐ │ │ ┌────────────────┐ │
│ │ import('remote/ │ │ │ │ exposes: { │ │
│ │ ./Button') │ │ │ │ './Button' │ │
│ └────────────────┘ │ │ └────────────────┘ │
│ shared: { │ │ shared: { │
│ react, │ │ react, │
│ react-dom │ │ react-dom │
│ } │ │ } │
└─────────────────────┘ └─────────────────────┘
核心原理:
- exposes:远程应用暴露给外部使用的模块
- remotes:主应用声明需要从远程应用加载的模块
- shared:声明共享依赖,避免重复打包(如 react/react-dom/Vue)
- 运行时加载:
import('remote_app/Module')在运行时发起 JSONP 请求,拉取远程应用的构建产物(remoteEntry.js)
3.2 Webpack Module Federation 配置
3.2.1 远程应用(Remote)—— 暴露模块
Vue 3 子应用(组件暴露):
// webpack.config.ts(Vue 3 子应用)
import { ModuleFederationPlugin } from 'webpack';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'product_app', // 应用唯一标识
filename: 'remoteEntry.js', // 入口文件名
exposes: {
'./ProductList': './src/components/ProductList.vue',
'./ProductDetail': './src/components/ProductDetail.vue',
'./useProducts': './src/composables/useProducts.ts',
},
shared: {
vue: {
singleton: true, // 全局单例
requiredVersion: '^3.4.0', // 版本约束
eager: false, // 懒加载
},
pinia: { singleton: true, requiredVersion: '^2.1.0' },
'element-plus': { singleton: true },
},
}),
],
};
React 子应用(组件暴露):
// webpack.config.ts(React 子应用)
new ModuleFederationPlugin({
name: 'user_app',
filename: 'remoteEntry.js',
exposes: {
'./UserProfile': './src/components/UserProfile.tsx',
'./UserList': './src/pages/UserList.tsx',
'./useAuth': './src/hooks/useAuth.ts',
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true },
zustand: { singleton: true },
antd: { singleton: true },
},
}),
3.2.2 主应用(Host)—— 消费远程模块
// webpack.config.ts(主应用)
new ModuleFederationPlugin({
name: 'main_app',
remotes: {
// key => 远程应用名,value => 远程入口 URL
product_app: 'product_app@http://localhost:3001/remoteEntry.js',
user_app: 'user_app@http://localhost:3002/remoteEntry.js',
},
shared: {
vue: { singleton: true, requiredVersion: '^3.4.0' },
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true },
pinia: { singleton: true },
zustand: { singleton: true },
},
}),
3.2.3 消费远程模块
在 Vue 3 主应用中加载 Vue 远程组件:
<!-- MainApp.vue -->
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
// 方式一:静态导入远程组件
const RemoteProductList = defineAsyncComponent(
() => import('product_app/ProductList')
);
// 方式二:动态按需加载
const remoteComponent = ref<Component | null>(null);
async function loadRemoteModule() {
try {
const module = await import('user_app/UserProfile');
remoteComponent.value = module.default;
} catch (error) {
console.error('远程模块加载失败:', error);
// 防御式编程:永远不要信任远程模块可用
remoteComponent.value = FallbackComponent;
}
}
</script>
<template>
<div>
<h3>远程模块示例</h3>
<Suspense>
<RemoteProductList />
<template #fallback>
<el-skeleton :rows="3" animated />
</template>
</Suspense>
</div>
</template>
在 React 主应用中加载 React 远程组件:
// App.tsx
import React, { Suspense, lazy, ComponentType } from 'react';
import { Spin } from 'antd';
// 方式一:静态 lazy 加载(推荐)
const RemoteUserList = lazy(() => import('user_app/UserList'));
// 方式二:动态加载 + 错误边界
function useRemoteModule<T>(loader: () => Promise<{ default: T }>) {
const [module, setModule] = React.useState<T | null>(null);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
loader()
.then((mod) => setModule(mod.default))
.catch((err) => {
console.error('远程模块加载失败:', err);
setError(err);
});
}, []);
return { module, error };
}
function App() {
const { module: UserProfile, error } = useRemoteModule(
() => import('user_app/UserProfile')
);
return (
<div>
<Suspense fallback={<Spin tip="加载远程模块..." />}>
<RemoteUserList />
</Suspense>
{error && <Alert message="远程服务不可用" type="warning" />}
</div>
);
}
3.3 Vite Module Federation
Vite 原生不支持 Module Federation,需要通过社区插件 @originjs/vite-plugin-federation。
// vite.config.ts(远程应用)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
vue(),
federation({
name: 'product_app',
filename: 'remoteEntry.js',
exposes: {
'./ProductCard': './src/components/ProductCard.vue',
'./store': './src/stores/productStore.ts',
},
shared: ['vue', 'pinia'],
}),
],
build: {
target: 'esnext', // 必须设置为 esnext
minify: false,
cssCodeSplit: false,
},
});
// vite.config.ts(主应用)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
vue(),
federation({
name: 'main_app',
remotes: {
product_app: 'http://localhost:5001/assets/remoteEntry.js',
},
shared: ['vue', 'pinia'],
}),
],
build: {
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
});
Vite Module Federation 注意事项:
| 注意点 | 说明 |
|---|---|
build.target 必须为 esnext |
否则 remoteEntry 会报语法错误 |
minify 建议关闭 |
产物体积稍大,但避免远程模块加载异常 |
cssCodeSplit 关闭 |
否则子应用样式不会随 remoteEntry 一起加载 |
| dev 模式不支持 | Vite 插件仅在生产构建时生效,开发阶段需通过其他方式联合调试 |
| 版本兼容 | @originjs/vite-plugin-federation 版本需与 Vite 版本匹配 |
3.4 共享依赖(shared)策略
shared 配置是 Module Federation 最核心也最易出错的配置。
shared: {
// ✅ 正确:指定版本范围,使用单例
vue: {
singleton: true,
requiredVersion: '^3.4.0',
eager: false,
},
// ❌ 错误:不指定版本,可能导致多个 Vue 实例
vue: {},
// ❌ 错误:singleton: false,导致重复加载
vue: { singleton: false },
// ✅ 场景2:需要立即加载的共享库
'core-js': { eager: true },
// ✅ 场景3:第三方 UI 组件库
'element-plus': { singleton: true, requiredVersion: '^2.8.0' },
antd: { singleton: true, requiredVersion: '^5.12.0' },
}
shared 配置选项详解:
| 选项 | 说明 | 推荐值 | 踩坑说明 |
|---|---|---|---|
singleton |
是否全局单例 | true (UI库/框架) |
false 会导致多个 Vue/React 实例,引发异常 |
requiredVersion |
版本约束 | ^x.y.z 宽松范围 |
过严导致加载失败,过松导致版本不兼容 |
eager |
是否立即加载(非异步 Chunk) | 工具库 true,UI 库 false |
true 增大首屏体积 |
import |
指定 fallback 模块 | 一般不设置 | 设置后会导致本地再打包一份 |
shared 配置原则:
┌─────────────────────────────────────────┐
│ shared 配置决策过程 │
├─────────────────────────────────────────┤
│ │
│ UI 框架(Vue/React) → singleton: true │
│ UI 组件库(Antd/Element) → singleton │
│ 状态管理(Pinia/Zustand) → singleton │
│ │
│ 工具库(lodash/dayjs) → 不共享或自愿 │
│ 业务组件 → 不共享(通过 exposes 暴露) │
│ │
└─────────────────────────────────────────┘
3.5 跨技术栈:Vue 应用加载 React 组件
Module Federation 天然支持跨框架加载,但需要约定组件接口。
React 远程组件(暴露统一 Props 接口):
// user_app/src/components/UserProfile.tsx
import React from 'react';
import { Card, Descriptions } from 'antd';
// 统一 Props 接口(Vue 侧通过 attrs 传递)
export interface UserProfileProps {
userId: string;
onLogout?: () => void;
}
const UserProfile: React.FC<UserProfileProps> = ({ userId, onLogout }) => {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then(setUser)
.catch(() => setUser(null)); // 防御式编程
}, [userId]);
return (
<Card>
<Descriptions title="用户信息">
<Descriptions.Item label="ID">{user?.id ?? '---'}</Descriptions.Item>
<Descriptions.Item label="姓名">{user?.name ?? '---'}</Descriptions.Item>
</Descriptions>
{onLogout && <button onClick={onLogout}>退出登录</button>}
</Card>
);
};
export default UserProfile;
Vue 主应用中加载:
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';
// Vue 加载 React 组件需要包裹层
const ReactUserProfile = defineAsyncComponent(
() => import('user_app/UserProfile')
);
const currentUserId = ref('12345');
function handleLogout() {
console.log('退出登录');
}
</script>
<template>
<div class="vue-container">
<h3>Vue 主应用加载 React 组件</h3>
<Suspense>
<!-- Vue 模板中直接使用 React 组件,Props 通过 kebab-case 传递 -->
<ReactUserProfile
:user-id="currentUserId"
:on-logout="handleLogout"
/>
<template #fallback>
<el-skeleton :rows="4" animated />
</template>
</Suspense>
</div>
</template>
跨框架通信约定:
| 约定 | 说明 | 示例 |
|---|---|---|
| Props 接口 | 使用纯 JavaScript 类型(string / number / function) | userId: string, onLogout: () => void |
| 避免传复杂对象 | 跨框架传递 Proxy 对象可能引发问题 | 传 string ID 而非 User 对象 |
| 避免传 reactive 对象 | Vue 的 ref/reactive 对 React 不可用 | 使用普通值或函数回调 |
| 事件回调 | 通过 Props 传递函数 | : 绑定即可 |
| 样式隔离 | 使用 CSS Modules 或 CSS-in-JS | 避免全局样式污染 |
四、Qiankun 实践
4.1 原理概述
Qiankun 基于 single-spa,在路由变化时加载/卸载子应用,核心机制:
- HTML Entry:主应用通过 fetch 加载子应用的 HTML,解析后提取 JS/CSS 执行
- Proxy 沙箱:通过 Proxy 劫持 window 对象,隔离子应用的全局变量
- 样式隔离:通过 Shadow DOM 或 Scoped CSS 隔离子应用样式
- 生命周期:子应用导出
bootstrap / mount / unmount三阶段
用户访问 /product
│
▼
主应用检测到路由变化,激活 product_app
│
├── fetch http://product-app/ → 加载 HTML Entry
├── 解析 HTML,提取 CSS / JS
├── 创建 Proxy 沙箱
├── 执行子应用 JS(在沙箱中)
├── 调用子应用 mount 生命周期
└── 渲染完成
4.2 主应用(基座)配置
// main-app/src/main.ts(Vue 3 主应用)
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import {
registerMicroApps,
start,
initGlobalState,
MicroAppStateActions,
} from 'qiankun';
const app = createApp(App);
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./views/Home.vue') },
// 子应用路由不需要在主应用中定义,由子应用自身管理
],
});
app.use(router);
app.use(createPinia());
app.use(ElementPlus);
app.mount('#main-app');
// ==================== Qiankun 配置 ====================
// 1. 注册子应用
registerMicroApps([
{
name: 'product-app', // 子应用名称(唯一)
entry: '//localhost:3001', // 子应用入口 HTML
container: '#sub-app-container', // 挂载容器
activeRule: '/product', // 激活路由规则
props: {
// 传递给子应用的自定义数据
globalToken: localStorage.getItem('token'),
},
},
{
name: 'order-app',
entry: '//localhost:3002',
container: '#sub-app-container',
activeRule: '/order',
props: {},
},
{
name: 'user-app',
entry: '//localhost:3003',
container: '#sub-app-container',
activeRule: '/user',
props: {},
},
]);
// 2. 全局状态(应用间通信)
const actions: MicroAppStateActions = initGlobalState({
user: null,
token: localStorage.getItem('token'),
});
// 监听状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('[主应用] 全局状态变化:', state, '->', prev);
});
// 主应用可主动更新状态
export function updateToken(token: string) {
actions.setGlobalState({ token });
}
// 3. 启动 Qiankun
start({
sandbox: {
experimentalStyleIsolation: true, // 开启样式隔离
},
prefetch: 'all', // 预加载所有子应用
});
主应用 App.vue 模板:
<!-- App.vue -->
<template>
<el-container>
<!-- 主应用导航 -->
<el-header>
<el-menu mode="horizontal" router>
<el-menu-item index="/">首页</el-menu-item>
<el-menu-item index="/product">商品管理</el-menu-item>
<el-menu-item index="/order">订单管理</el-menu-item>
<el-menu-item index="/user">用户管理</el-menu-item>
</el-menu>
</el-header>
<el-main>
<!-- 主应用自身路由 -->
<router-view />
<!-- 子应用挂载容器 -->
<div id="sub-app-container"></div>
</el-main>
</el-container>
</template>
4.3 子应用注册
Vue 3 子应用
// sub-app/src/main.ts(Vue 3 子应用)
import { createApp, App as VueApp } from 'vue';
import { createRouter, createWebHistory, Router } from 'vue-router';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import routes from './router';
import type { QiankunProps } from './types';
let app: VueApp | null = null;
let router: Router | null = null;
// 子应用的生命周期导出(挂载到 window 上)
export async function bootstrap() {
console.log('[子应用] product-app bootstrapped');
}
export async function mount(props: QiankunProps) {
console.log('[子应用] product-app mounted, props:', props);
const { container, globalState } = props;
// 创建独立的 Vue 实例
app = createApp(App);
router = createRouter({
// 关键:使用 window.__POWERED_BY_QIANKUN__ 判断,添加路由前缀
history: createWebHistory(
window.__POWERED_BY_QIANKUN__ ? '/product' : '/'
),
routes,
});
app.use(router);
app.use(createPinia());
app.use(ElementPlus);
// 注入 Qiankun 传递的全局状态
if (globalState) {
const store = useGlobalStore();
store.setToken(globalState.token);
}
// 挂载到子应用容器(注意:使用 container 下的选择器)
app.mount(
container
? container.querySelector('#sub-app')
: document.getElementById('sub-app')
);
}
export async function unmount(props: QiankunProps) {
console.log('[子应用] product-app unmounted');
app?.unmount();
app = null;
router = null;
}
// sub-app/vue.config.ts(Qiankun 子应用 webpack 配置)
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*', // 允许跨域加载
},
},
configureWebpack: {
output: {
library: 'product-app',
libraryTarget: 'umd', // 必须 UMD 格式
chunkLoadingGlobal: 'webpackJsonp_product_app', // 避免 jsonp 冲突
},
},
});
React 子应用
// sub-app/src/public-path.ts(必须第一行引入)
if ((window as any).__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// sub-app/src/index.ts(React 子应用入口)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './public-path';
let root: ReactDOM.Root | null = null;
export async function bootstrap() {
console.log('[React 子应用] bootstrapped');
}
export async function mount(props: any) {
const { container, globalState } = props;
root = ReactDOM.createRoot(
container
? container.querySelector('#root')
: document.getElementById('root')
);
root.render(
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/order' : '/'}>
<App globalState={globalState} />
</BrowserRouter>
);
}
export async function unmount(props: any) {
root?.unmount();
root = null;
}
4.4 样式隔离
Qiankun 提供两种样式隔离方案:
| 方案 | 配置 | 原理 | 优缺点 |
|---|---|---|---|
| StrictStyleIsolation | sandbox.strictStyleIsolation: true |
Shadow DOM 包裹 | 完全隔离,但部分 UI 库弹窗挂载在 body 下导致样式丢失 |
| ExperimentalStyleIsolation | sandbox.experimentalStyleIsolation: true |
添加属性选择器(div[data-qiankun-app]) |
兼容性好,但 CSS 选择器权重改变可能覆盖异常 |
// ✅ 推荐:使用 ExperimentalStyleIsolation
start({
sandbox: {
experimentalStyleIsolation: true,
},
});
// ❌ 不推荐:StrictStyleIsolation 会导致弹窗/Select Dropdown 样式异常
样式隔离踩坑点:
<!-- ❌ 问题:子应用中的弹窗挂载在 body 下,Shadow DOM 无法控制 -->
<template>
<el-dialog v-model="visible" title="提示">
<!-- 弹窗 DOM 被追加到 document.body,不在 Shadow DOM 内 -->
<!-- 子应用样式无法应用到弹窗上 -->
</el-dialog>
</template>
<!-- ✅ 解决:使用 append-to-body="false" 将弹窗挂载到当前容器 -->
<template>
<el-dialog
v-model="visible"
title="提示"
:append-to-body="false"
>
</el-dialog>
</template>
// React 同理:Ant Design Modal 挂载点问题
<Modal
open={visible}
getContainer={() => document.getElementById('sub-app-root')!}
// ✅ 关键:指定挂载容器,避免跑到 Shadow DOM 外
>
<p>弹窗内容</p>
</Modal>
4.5 JS 沙箱
Qiankun 的沙箱基于 Proxy 实现,有三种沙箱类型:
| 沙箱类型 | 配置 | 支持多实例 | 性能 | 特性 |
|---|---|---|---|---|
| LegacySandbox | 单例模式 | ❌ | 高 | 单应用场景 |
| ProxySandbox | 默认 | ✅ | 中 | 多应用共存场景(推荐) |
| SnapshotSandbox | 不兼容 Proxy 时降级 | ❌ | 低 | 兼容 IE11 |
start({
sandbox: {
// 默认使用 ProxySandbox
// 以下配置为严格模式下的选项
strictStyleIsolation: false,
experimentalStyleIsolation: true,
},
});
沙箱原理简析:
// 伪代码:Proxy 沙箱简化版
class ProxySandbox {
private proxyWindow: Window;
constructor() {
const fakeWindow = Object.create(null);
this.proxyWindow = new Proxy(fakeWindow, {
get(target, key) {
// 优先返回 fakeWindow 上的属性
if (Reflect.has(target, key)) {
return Reflect.get(target, key);
}
// 回退到真实 window
return (window as any)[key];
},
set(target, key, value) {
// 子应用的全局变量写入 fakeWindow,不污染真实 window
return Reflect.set(target, key, value);
},
});
}
}
沙箱相关问题排查:
// ❌ 问题1:子应用使用 window.xxx 存储全局变量,卸载后未清理
// ✅ 解决:卸载时在 unmount 中清理
export async function unmount() {
// 清理定时器
clearInterval(timer);
// 清理全局事件
window.removeEventListener('resize', handleResize);
// 清理全局变量(Qiankun 沙箱会自动清理,但最好手动)
delete (window as any).__MY_GLOBAL__;
}
// ❌ 问题2:子应用使用了非标准 Window 属性
const el = window as any;
el.CustomGlobalVar = 'xxx'; // 可能被 Qiankun 沙箱拦截
// ✅ 解决:使用提供的 props 或状态管理
const props = useQiankunProps();
props.setGlobalState({ key: 'xxx' });
4.6 应用间通信
方式一:initGlobalState(官方方案)
// 主应用
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, token: '' });
// 主应用更新
actions.setGlobalState({ user: { id: 1, name: '张三' } });
// 子应用获取
export async function mount(props: any) {
// props 包含 onGlobalStateChange 和 setGlobalState
props.onGlobalStateChange((state: any, prev: any) => {
console.log('全局状态变化:', state);
// 同步到子应用的 Store
useUserStore().setUser(state.user);
}, true); // true:立即触发一次
// 子应用也可以修改全局状态
props.setGlobalState({ token: 'new-token' });
}
方式二:shared 模块(推荐,更灵活)
在 Module Federation 中使用 shared 暴露通信模块:
// shared-lib/src/communication.ts(独立的公共仓库)
import { reactive } from 'vue';
// 全局状态中心
export const globalStore = reactive<{
user: { id: string; name: string } | null;
token: string;
events: Map<string, Function[]>;
}>({
user: null,
token: '',
events: new Map(),
});
// 发布订阅通信
export function on(event: string, callback: Function) {
if (!globalStore.events.has(event)) {
globalStore.events.set(event, []);
}
globalStore.events.get(event)!.push(callback);
// 返回取消订阅函数
return () => {
const cbs = globalStore.events.get(event);
if (cbs) {
const idx = cbs.indexOf(callback);
if (idx > -1) cbs.splice(idx, 1);
}
};
}
export function emit(event: string, ...args: any[]) {
globalStore.events.get(event)?.forEach((cb) => cb(...args));
}
跨技术栈通信方案对比:
| 方式 | 适用场景 | 复杂度 | 风险 |
|---|---|---|---|
initGlobalState |
Qiankun 体系内 | 低 | 状态多时维护困难 |
shared 模块 |
Module Federation | 中 | 需保证 singleton |
CustomEvent |
任意方案 | 低 | 事件名冲突,无类型安全 |
URL 参数 |
简单数据传递 | 最低 | 只能传字符串 |
| 后端统一状态 | 登录态等全局数据 | 中 | 增加网络开销 |
五、微前端实战要点
5.1 公共依赖提取
方案对比:
| 策略 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| CDN 外链 | 框架/Vue/React 走 CDN,不打包 | 减小体积 | 版本锁定,升级不便 |
| shared(Module Federation) | 运行时共享依赖 | 按需加载,版本协商 | 配置复杂 |
| External 配置 | webpack externals 排除框架 | 体积最小 | 需要手动管理 CDN |
| 公共 NPM 包 | 公共逻辑抽为独立包,子应用引用 | 类型安全 | 版本升级需要所有子应用重新构建 |
推荐做法:
// 方案:Module Federation shared + external 混合
// 子应用 webpack 配置
module.exports = {
externals: {
// 将大型 UI 库设为 external,避免重复打包
'element-plus': 'ElementPlus',
vue: 'Vue',
},
};
公共依赖提取原则:
| 依赖类型 | 建议 | 原因 |
|---|---|---|
| Vue / React 框架 | Webpack external + CDN | 体积大(>100KB),版本统一 |
| UI 组件库 | shared singleton | 组件多,提取后大幅减少体积 |
| 工具库(lodash/dayjs) | 子应用自包含 | 体积小,版本差异影响小 |
| 业务公共组件 | remote exposes | 统一管理,避免重复实现 |
| 状态管理库 | shared singleton | 保证 Store 是同一实例 |
5.2 路由分发
核心问题: 主应用路由与子应用路由如何协同工作?
方案:主应用控制路由前缀,子应用管理内部路由
URL 设计:
/
├── /product → 激活 Product 子应用
│ ├── /product/list → 商品列表(子应用内部路由)
│ └── /product/:id → 商品详情(子应用内部路由)
├── /order → 激活 Order 子应用
│ ├── /order/list → 订单列表(子应用内部路由)
│ └── /order/:id → 订单详情(子应用内部路由)
└── /user → 激活 User 子应用
└── /user/profile → 用户信息(子应用内部路由)
Qiankun 路由创建:
// 子应用:根据是否在 Qiankun 中添加 basename
// Vue Router
const router = createRouter({
history: createWebHistory(
window.__POWERED_BY_QIANKUN__ ? '/product' : '/'
),
routes,
});
// React Router
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/order' : '/'}>
<App />
</BrowserRouter>
路由分发注意事项:
// ✅ 正确:主应用不重复定义子应用路由
const routes = [
{ path: '/', component: Home },
// 不需要定义 /product/** 的路由,Qiankun 通过 activeRule 控制
];
// ❌ 错误:主应用和子应用都定义相同路由,导致重复渲染
const routes = [
{ path: '/product', component: ProductApp }, // 错误!
];
// ✅ 正确:主应用只提供一个容器,路由匹配由 Qiankun 的 activeRule 决定
5.3 登录态共享
推荐方案:SSO + Token 透传
用户登录
│
▼
主应用获取 Token (JWT)
│
├── Token 存入 localStorage(同一域名下)
├── 通过 Qiankun props 或 shared 状态传递到子应用
├── 子应用从 props/globalState 获取 Token
│
▼
子应用发请求时携带 Token(Authorization Header)
│
▼
后端验证 Token,返回数据
主应用传递 Token:
// 主应用:登录后更新
const actions = initGlobalState({
token: localStorage.getItem('token') || '',
user: null,
});
// Token 续期
function refreshToken(newToken: string) {
localStorage.setItem('token', newToken);
actions.setGlobalState({ token: newToken });
}
子应用获取 Token:
// 子应用:从 Qiankun props 获取
export async function mount(props: any) {
const { token } = props;
// 方式1:直接设置请求头
request.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// 方式2:存入本地状态管理
const authStore = useAuthStore();
authStore.setToken(token);
}
// 方式3(推荐):封装请求库中自动提取
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
// 请求拦截器:自动携带 Token
request.interceptors.request.use((config) => {
// 优先从 Qiankun 全局状态获取
let token = '';
if (window.__POWERED_BY_QIANKUN__) {
// 通过 props 获取(需要从 store 或全局变量读取)
token = useAuthStore().token;
} else {
// 独立运行时从 localStorage 获取
token = localStorage.getItem('token') || '';
}
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
5.4 部署与版本管理
独立部署架构:
CDN (OSS / CloudFront)
│
├── /main-app/ ← 主应用(基座)
│ ├── index.html
│ ├── assets/main.xxx.js
│ └── assets/main.xxx.css
│
├── /product-app/ ← 子应用 A(Vue 3)
│ ├── index.html
│ ├── assets/product.xxx.js
│ └── remoteEntry.js ← Module Federation 入口
│
├── /order-app/ ← 子应用 B(React)
│ ├── index.html
│ ├── assets/order.xxx.js
│ └── remoteEntry.js ← Module Federation 入口
│
└── /user-app/ ← 子应用 C
└── ...
版本管理策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 同版本发布 | 主应用+子应用统一版本 | 小团队,发布频率一致 |
| 独立版本+兼容 | 子应用各自版本,主应用通过配置引用 | 大团队,独立迭代 |
| 灰度发布 | 主应用维护多版本 remoteEntry 映射 | 大型平台,需要灰度能力 |
版本管理示例(主应用配置化):
// main-app/src/config/apps.ts
// 主应用通过配置文件管理子应用版本,实现灰度发布
interface AppConfig {
name: string;
entry: string;
activeRule: string;
version: string;
grayUsers?: string[]; // 灰度用户白名单
}
const appConfigs: Record<string, AppConfig> = {
'product-app': {
name: 'product-app',
entry: '//cdn.example.com/product-app/2.1.0/', // 通过版本号控制
activeRule: '/product',
version: '2.1.0',
grayUsers: ['user_001'], // 灰度用户走 2.2.0-beta
},
'order-app': {
name: 'order-app',
entry: '//cdn.example.com/order-app/1.3.0/',
activeRule: '/order',
version: '1.3.0',
},
};
5.5 CI/CD 独立部署流程
子应用独立部署流程:
┌──────────┐ ┌───────────┐ ┌──────────┐ ┌───────────┐
│ 开发者 │ │ CI/CD │ │ CDN │ │ 主应用 │
│ push代码 │ │ 构建 │ │ 上传 │ │ 加载 │
└────┬─────┘ └─────┬─────┘ └────┬─────┘ └─────┬─────┘
│ │ │ │
│── git push ───→│ │ │
│ │── npm run build → │ │
│ │ │── upload /dist │
│ │ │──→ CDN 资源 │
│ │ │ │
│ │── 通知主应用 │ │
│ │ 版本已更新 │ │
│ │───────────────│────────────────│
│ │ │ │── 用户访问
│ │ │── 加载新版 │← 加载新资源
│ │ │ remoteEntry │
CI/CD 配置示例(GitHub Actions):
# .github/workflows/product-app-deploy.yml
name: Deploy Product App
on:
push:
branches: [main]
paths: # 只有 product-app 目录变化才触发
- 'apps/product-app/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm
run: npm install -g pnpm
- name: Install dependencies
run: pnpm install
- name: Build product app
run: pnpm --filter product-app build
- name: Upload to CDN
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ secrets.CDN_BUCKET }}
SOURCE_DIR: 'apps/product-app/dist'
DEST_DIR: 'product-app/${{ github.sha }}' # 版本号
- name: Update version config
run: |
# 更新主应用的版本配置(通过 API 或更新配置文件)
curl -X POST https://api.example.com/config/update \
-H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
-d '{"app":"product-app","version":"${{ github.sha }}"}'
六、微前端 vs Monorepo 选型
6.1 对比维度
| 维度 | 微前端 | Monorepo |
|---|---|---|
| 仓库数量 | 多个独立仓库(Multi-repo) | 单一仓库管理多个项目 |
| 构建方式 | 独立构建,独立部署 | 统一构建,整体部署 |
| 发布粒度 | 子应用单独发布 | 一次构建发布所有包 |
| 技术栈 | 任意技术栈混用 | 技术栈必须统一 |
| 代码共享 | 通过模块联邦/NPM 包 | 通过 workspace 直接引用 |
| 依赖管理 | 各自管理,可能重复 | 统一管理,依赖提升 |
| 团队协作 | 完全隔离,代码不共享 | 代码互访,便于重构 |
| 构建时间 | 小(单应用 < 30s) | 大(全量构建 > 10min) |
| 运行时隔离 | 沙箱隔离,样式/JS 互不干扰 | 无隔离,同一运行时 |
| 学习成本 | 高(沙箱/通信/部署复杂) | 低(常规开发模式) |
| 调试难度 | 高(跨应用调试困难) | 低(IDE 直接调试) |
6.2 工程复杂度对比
复杂度曲线:
▲
│ ╱ 微前端
工程复杂度 │ ╱
│ ╱────────── Monorepo
│ ╱
│ ╱────────── 单体前端
│ ╱
└─────────────────────────────→
团队规模
6.3 选型决策
| 条件 | 推荐方案 | 原因 |
|---|---|---|
| 1~5 人团队,代码 < 20 万行 | 单体前端 | 简单直接,复杂度最低 |
| 1~10 人团队,技术栈统一,多包复用 | Monorepo + pnpm workspace | 代码复用好,构建可优化 |
| 10+ 人团队,多业务域,独立部署需求 | 微前端 | 独立开发、独立部署、独立运维 |
| 10+ 人团队,但技术栈统一,高频协作 | Monorepo + Module Federation | 结合两者优点 |
| 存量系统渐进迁移 | 微前端(Qiankun / Micro-app) | 新老共存,逐步替换 |
6.4 推荐:Monorepo + Module Federation 混合方案
# 实践中的最佳组合
仓库组织:Monorepo(pnpm workspace)
│
├── packages/
│ ├── shared-lib/ # 公共工具库
│ ├── ui-components/ # 公共 UI 组件
│ └── config/ # 公共配置
│
├── apps/
│ ├── main-app/ # 主应用(基座)
│ ├── product-app/ # 商品子应用
│ └── order-app/ # 订单子应用
│
构建方式:
├── dev 模式 → pnpm --filter 独立启动
├── 公共包变更 → pnpm build:changed(只构建受影响的包)
└── 子应用发布 → 独立构建 + CDN 上传
Monorepo + Module Federation 的好处:
| 优势 | 说明 |
|---|---|
| 源码共享 | 公共类型/工具通过 workspace 引用,无需发布 NPM 包 |
| 开发体验 | 本地可同时启动多个子应用,统一调试入口 |
| 独立部署 | 每个子应用可独立构建、独立发布到 CDN |
| 共享受限 | 运行时通过 Module Federation shared 配置共享框架依赖 |
| 渐进迁移 | 新技术可以从一个子应用开始,不影响其他模块 |
七、常见问题与踩坑记录
7.1 Module Federation 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
Shared module is not available for eager consumption |
shared 模块的 eager 配置不一致 | 确保主应用/远程应用的 shared 配置一致,或关闭 eager |
| 远程模块加载 404 | remoteEntry.js 路径配置错误 | 检查 remotes 中的 URL 是否为完整可访问路径 |
| 版本不兼容导致加载失败 | shared 的 requiredVersion 过于严格 | 放宽版本范围,如 ^3.x.x |
| Vue / React 多实例 | shared singleton: false 或版本不一致 | 确保 singleton: true,且版本范围兼容 |
| CSS 丢失 | Vite 构建未关闭 cssCodeSplit | cssCodeSplit: false |
| TypeError: Cannot read properties of null (reading 'call') | 远程应用未正确暴露模块 | 检查 exposes 配置以及构建产物 |
7.2 Qiankun 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 子应用切换后内存泄漏 | 定时器/事件监听未在 unmount 清理 | 在 unmount 中清理所有副作用 |
| 样式冲突 | 主应用和子应用使用相同 CSS 类名 | 开启 experimentalStyleIsolation,或子应用 CSS 加 Scope |
| 子应用弹窗显示异常 | 弹窗挂载到 body 导致样式隔离失效 | 使用 append-to-body / getContainer 指定容器 |
| Webpack JSONP 冲突 | 多个子应用使用相同 chunkLoadingGlobal | 每个子应用设置唯一的 chunkLoadingGlobal |
| 子应用路由不生效 | basename 未根据 Qiankun 环境动态设置 | 通过 window.__POWERED_BY_QIANKUN__ 判断添加 basename |
| 开发环境子应用加载失败 | CORS 跨域限制 | 子应用配置 devServer.headers['Access-Control-Allow-Origin'] = '*' |
7.3 通用调试 Checklist
□ 确认主应用已成功启动 Qiankun(start() 被调用)
□ 确认子应用正确导出了 bootstrap/mount/unmount 生命周期
□ 确认子应用的 webpack output.library 和 libraryTarget 配置正确
□ 确认子应用 remoteEntry.js/CDN 地址可正常访问
□ 确认 Shared 依赖的版本范围兼容(singleton: true)
□ 确认子应用的 basename 路由配置正确(__POWERED_BY_QIANKUN__)
□ 确认样式隔离模式下弹窗/Select 下拉框的挂载容器配置
□ 确认子应用无未清理的定时器、事件监听(内存泄漏检查)
□ 确认跨域相关 Header(CORS)已正确配置
□ 确认 micro-app 或 Qiankun 的版本与框架版本兼容
八、总结
8.1 最终选型建议
| 场景 | 推荐方案 | 核心理由 |
|---|---|---|
| 阿里系中台,Vue 技术栈 | Qiankun | 生态完善,文档齐全,阿里内部大量验证 |
| 需要零改造接入存量系统 | Micro-app / Wujie | WebComponent 方案,子应用几乎无感知 |
| 技术栈统一,追求极致性能 | Module Federation | 零沙箱开销,native 模块加载,SSR 支持 |
| 新项目,团队 5~15 人 | Monorepo + pnpm + Module Federation | 兼顾开发体验和运行时性能 |
| 大型平台,多团队,多技术栈 | Qiankun + Module Federation(互补) | Qiankun 做沙箱隔离,MF 做模块共享 |
8.2 核心原则
1. 不要为了微前端而微前端 —— 引入微前端意味着增加复杂度
2. 沙箱隔离不是免费的 —— Proxy 沙箱和 Shadow DOM 都有性能开销
3. 子应用间通信越少越好 —— 全局状态应该尽量精简
4. 版本兼容性是最大挑战 —— 建立完善的版本管理和兼容性测试机制
5. 部署自动化是前提 —— 没有独立部署能力就不要谈微前端
6. 优先考虑 Monorepo + Module Federation 的轻量方案