Appearance
微前端
本手册从"为什么需要微前端"出发,逐步深入到技术方案选型、主流框架原理、qiankun 核心实现(JS 沙箱 / CSS 隔离 / 生命周期)以及应用通信与实战落地,帮助你构建完整的微前端知识体系。
导航目录
一、概念篇
二、方案对比篇
三、框架对比篇
四、qiankun 原理篇
五、通信与实战篇
为什么需要微前端?
核心概念
微前端(Micro Frontends) 是一种将大型前端应用按业务维度拆分成多个可独立开发、独立部署的子应用,再由一个主应用(基座)动态加载并整合的架构模式。它把后端"微服务"的分而治之思想搬到了前端。
随着业务膨胀,单体前端应用(Monolith)会逐渐暴露出一系列问题:
| 单体应用痛点 | 具体表现 |
|---|---|
| 代码库臃肿 | 几十万行代码堆在一个仓库,编译一次要几分钟,改一行代码全量构建 |
| 技术栈锁死 | 项目起步用 Vue2,想升级 Vue3 / React 几乎不可能,只能推倒重来 |
| 团队协作冲突 | 多个团队共用一个仓库,代码合并冲突频繁,发布互相阻塞 |
| 牵一发动全身 | 任意模块出错可能拖垮整个应用,发布风险高 |
| 老系统难维护 | 遗留系统技术过时,但重写成本极高,无法渐进式改造 |
生活类比
把单体应用想象成一栋一体浇筑的大楼——想改一间房就得动整栋结构。微前端则像乐高积木搭的大楼:每一块(子应用)可以独立设计、独立更换,拼起来仍是一栋完整的楼。
实现思路
将应用按业务域划分为若干子应用,各自独立打包成可被远程加载的模块;主应用(基座)负责统一的路由分发,在路由切换时动态加载 / 卸载对应的子应用,从而在保持技术独立性的同时,对用户呈现为一个完整的产品。
┌─────────────────────────────┐
│ 主应用 / 基座 │
│ (路由分发 · 全局状态 · 布局) │
└──────────────┬──────────────┘
┌───────────┼───────────┐
┌────▼────┐ ┌───▼────┐ ┌───▼─────┐
│ 子应用A │ │ 子应用B │ │ 子应用C │
│ (Vue3) │ │ (React) │ │ (Angular)│
└─────────┘ └────────┘ └─────────┘微前端的核心价值与适用场景
核心价值
- 技术栈无关:主框架不限制接入子应用的技术栈,Vue、React、Angular 甚至 jQuery 老项目都能共存
- 独立开发部署:每个子应用独立仓库、独立 CI/CD,发布互不影响,缩短交付周期
- 渐进式迁移:老系统可以"包"进微前端框架,一个模块一个模块地重写,降低升级风险
- 团队自治:各团队按自己的节奏迭代,边界清晰,减少沟通成本
- 增量升级:可以在不影响其它部分的情况下,单独升级某个子应用的框架版本
微前端不是银弹
微前端引入了额外的复杂度(沙箱、通信、样式隔离、部署编排等)。如果你的应用不大、团队只有一两个人、技术栈统一,那么普通的路由拆分 / 组件库 / Monorepo 往往是更划算的选择。不要为了微前端而微前端。
适用场景
| 适合 ✅ | 不适合 ❌ |
|---|---|
| 大型中后台系统(多个业务模块) | 小型单页应用 |
| 多团队协作、技术栈不统一 | 单人 / 小团队、技术栈统一 |
| 老系统渐进式改造、遗留系统整合 | 对首屏性能极致敏感的 C 端页面 |
| 需要独立发布、灰度不同模块 | 模块间交互极其频繁、耦合极深 |
微前端要解决的三大核心问题
无论采用哪种技术方案,微前端框架本质上都在解决以下三个问题:
三大核心问题
- 应用加载:如何在运行时把子应用的 HTML / JS / CSS 拉取下来并渲染到主应用指定的容器中?
- 应用隔离:如何保证多个子应用之间、子应用与主应用之间的 JS 全局变量 与 CSS 样式 互不污染?
- 应用通信:拆分后的子应用之间如何安全、高效地共享状态与传递数据?
| 问题 | 关键技术 | 典型方案 |
|---|---|---|
| 应用加载 | HTML Entry / JS Entry、动态 script、import-html-entry | qiankun 的 HTML Entry |
| JS 隔离 | 快照沙箱、Proxy 沙箱、iframe 原生沙箱 | qiankun Proxy 沙箱、无界 iframe |
| CSS 隔离 | Shadow DOM、CSS Scoped、动态样式表增删 | micro-app WC、qiankun 严格样式隔离 |
| 应用通信 | props、CustomEvent、发布订阅(EventBus)、全局状态 | qiankun GlobalState、无界 EventBus |
后续章节将围绕这三大问题,逐一展开各方案与框架的实现细节。
iframe 方案
实现原理
通过 <iframe> 标签加载子应用页面,利用浏览器原生的沙箱机制天然实现 JS 与 CSS 的完全隔离,应用间通过 postMessage API 通信。
html
<iframe src="//sub-app.com/app1" id="sub"></iframe>
<script>
// 主应用 → 子应用
const iframe = document.getElementById("sub");
iframe.contentWindow.postMessage({ type: "login", token: "xxx" }, "*");
// 子应用监听
window.addEventListener("message", (e) => {
if (e.data.type === "login") console.log(e.data.token);
});
</script>| 优势 ✅ | 局限性 ❌ |
|---|---|
| 实现极简,技术成熟 | 弹窗 / 遮罩层被限制在 iframe 内,无法覆盖全屏 |
| 天然完美的 JS / CSS 隔离 | URL 不同步,浏览器前进后退状态丢失、刷新回到首页 |
| 安全边界清晰 | 每次切换重建 iframe,DOM / JS 上下文全部销毁,白屏明显 |
| 无需改造子应用 | 主子应用 DOM 割裂,共享弹窗、loading 困难 |
iframe 的致命伤
iframe 的隔离性无可挑剔,但用户体验问题(URL 不同步、弹窗受限、加载慢)使其难以承载复杂应用。"无界"框架正是通过 iframe + Web Components 的组合来扬长避短。
Web Components 方案
实现原理
基于浏览器原生的自定义元素(Custom Elements) 将子应用封装为一个 HTML 标签,利用 Shadow DOM 提供样式与 DOM 作用域隔离,通过 CustomEvent 进行通信。
js
// 定义子应用为自定义元素
class MicroApp extends HTMLElement {
connectedCallback() {
// 开启 Shadow DOM,样式天然隔离
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `<style>h1{color:red}</style><h1>子应用</h1>`;
}
}
customElements.define("micro-app", MicroApp);html
<micro-app url="//localhost:3000"></micro-app>| 优势 ✅ | 局限性 ❌ |
|---|---|
| 浏览器原生支持,无框架依赖 | 老浏览器兼容性差(IE 不支持) |
| Shadow DOM 天然样式隔离 | Shadow DOM 内某些第三方 UI 库(弹窗挂 body)失效 |
| 良好的组件封装性 | 学习曲线较陡,调试相对困难 |
TIP
micro-app 框架就是以 Web Components 为核心,把子应用包装成 <micro-app> 标签,做到"像用组件一样用微前端",接入几乎零改造。
single-spa 方案
实现原理
single-spa 是微前端的"鼻祖"框架。核心是劫持路由:监听 hashchange / popstate,根据当前 URL 匹配 activeRule,决定加载 / 卸载哪个子应用;子应用需暴露 bootstrap、mount、unmount 三个生命周期钩子。
js
import { registerApplication, start } from "single-spa";
registerApplication({
name: "app1",
app: () => System.import("app1"), // 用 SystemJS 加载
activeWhen: (location) => location.pathname.startsWith("/app1"),
customProps: { token: "xxx" },
});
start();| 优势 ✅ | 局限性 ❌ |
|---|---|
| 框架无关,技术栈灵活 | 不提供 JS / CSS 沙箱,需自行实现隔离 |
| 微前端生态的事实标准 | 子应用需较多改造(导出生命周期、改打包配置) |
| 支持渐进式迁移 | 需依赖 SystemJS,学习成本高 |
single-spa 的短板
single-spa 只解决了"应用加载与调度",没有解决隔离问题。这正是 qiankun 的价值所在——qiankun = single-spa(调度)+ import-html-entry(HTML Entry 加载)+ Proxy 沙箱(隔离)。
Module Federation 方案
实现原理
模块联邦(Module Federation,MF) 是 Webpack 5 的构建时特性。它允许一个应用(remote)在运行时将自己的模块暴露出去,另一个应用(host)直接远程加载并运行这些模块,还能共享依赖(如只加载一份 React)。
js
// 子应用(remote)webpack.config.js
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js",
exposes: { "./Button": "./src/Button" }, // 暴露模块
shared: ["react", "react-dom"], // 共享依赖
});
// 主应用(host)
new ModuleFederationPlugin({
name: "host",
remotes: { app1: "app1@//localhost:3001/remoteEntry.js" },
shared: ["react", "react-dom"],
});js
// 主应用中像本地模块一样使用
const RemoteButton = React.lazy(() => import("app1/Button"));| 优势 ✅ | 局限性 ❌ |
|---|---|
| 构建时优化,依赖共享,性能好 | 强依赖 Webpack 5(或 Vite 插件) |
| 模块级复用,不止是应用级 | 无沙箱隔离,全局变量 / 样式仍可能冲突 |
| 开发体验友好,像用 npm 包一样 | 版本 / 依赖协商配置较复杂 |
方案总览
- iframe:隔离满分,体验不及格
- Web Components:原生优雅,兼容性受限
- single-spa:调度标准,隔离缺失
- Module Federation:共享复用强,隔离缺失
现代企业级框架(qiankun / micro-app / 无界)本质是把上述方案组合 + 补齐短板后的工程化产物。
qiankun / micro-app / 无界 定位
| 框架 | 出品方 | 底层方案 | 一句话定位 |
|---|---|---|---|
| qiankun | 蚂蚁金服 | single-spa + import-html-entry + Proxy 沙箱 | 应用级微前端,生态最成熟、落地最广 |
| micro-app | 京东零售 | Web Components + 自研沙箱 | 组件式微前端,接入极简、几乎零改造 |
| 无界(wujie) | 腾讯 | iframe + Web Components | iframe 级隔离 + WC 级体验,主打稳定安全 |
qiankun
INFO
基于 single-spa 的企业级微前端解决方案,在阿里内部有大规模落地。核心是基于路由的应用加载与切换,配套 HTML Entry、JS 沙箱、样式隔离、预加载、全局状态等完整能力。子应用需导出生命周期钩子并调整打包配置。
micro-app
INFO
京东零售开源,把子应用封装成 <micro-app> 自定义元素,追求"像用 iframe 一样简单,像用组件一样自然"。子应用几乎零改造,路由自动同步,样式借助 Web Components 天然隔离。
无界(wujie)
INFO
腾讯开源,用 iframe 承载子应用的 JS 运行环境(拿到 iframe 级的完美隔离),用 Web Components 承载子应用的 DOM(解决 iframe 体验差的问题),子应用完全零改造,兼顾隔离与体验。
核心能力横向对比
技术原理与隔离
| 维度 | qiankun | micro-app | 无界 |
|---|---|---|---|
| 技术原理 | 路由劫持 + HTML Entry | Web Components + 自研沙箱 | iframe + Web Components |
| JS 沙箱 | Proxy 沙箱(多例) | 基于 Proxy 的沙箱 | iframe 原生沙箱 |
| CSS 隔离 | Shadow DOM / 严格样式隔离 | WC 天然隔离 | iframe 完全隔离 |
| 路由同步 | 手动配置 activeRule | 自动同步 | 自动同步 |
| 子应用改造 | 需导出生命周期 + 改打包 | 几乎零改造 | 完全零改造 |
接入成本
| 维度 | qiankun | micro-app | 无界 |
|---|---|---|---|
| 子应用改造 | 需要生命周期改造 | 几乎零改造 | 完全零改造 |
| 配置复杂度 | 中等 | 简单 | 简单 |
| 学习成本 | 较高 | 较低 | 较低 |
| 接入时间 | 1~2 天 | 几小时 | 几小时 |
隔离能力评分
| 能力 | qiankun | micro-app | 无界 |
|---|---|---|---|
| JS 隔离 | ⭐⭐⭐⭐⭐ Proxy 多例 | ⭐⭐⭐⭐ Proxy | ⭐⭐⭐⭐⭐ iframe 级 |
| CSS 隔离 | ⭐⭐⭐⭐ 可配置 | ⭐⭐⭐⭐⭐ WC 天然 | ⭐⭐⭐⭐⭐ iframe 级 |
| 多实例并存 | 支持 | 支持 | 支持(互不影响) |
通信机制
| 框架 | 通信方式 | 示例 |
|---|---|---|
| qiankun | props + GlobalState | props.onGlobalStateChange(cb) |
| micro-app | CustomEvent(setData / dispatch) | microApp.setData('app1', {...}) |
| 无界 | EventBus(发布订阅) | window.$wujie.bus.$emit('event', data) |
性能与生态
| 维度 | qiankun | micro-app | 无界 |
|---|---|---|---|
| 加载性能 | 中等(需框架 + 沙箱初始化) | 较好(轻量 + WC) | 优秀(接近原生 iframe) |
| 运行时性能 | 中等(Proxy 有开销) | 较好 | 优秀(iframe 级隔离) |
| 社区活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 文档完善度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 企业案例 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
如何选型?
选型结论
- 求稳定、功能全、生态成熟 → 选 qiankun(大型中后台首选,愿意付出接入成本换完整能力)
- 求快速、轻量、接入简单 → 选 micro-app(京东系、追求极简接入)
- 求安全、零改造、隔离彻底 → 选 无界(老系统 / 第三方页面嵌入场景)
选型时的关键决策链:
子应用能否改造?
├─ 不能改造(第三方 / 遗留系统)→ 无界 / micro-app
└─ 能改造
├─ 需要成熟生态 + 复杂场景 → qiankun
└─ 追求极简接入 → micro-app通用避坑
无论选哪个框架,都要提前规划:子应用静态资源的跨域 CORS、publicPath 动态设置、路由 base 前缀、样式隔离策略。这几点是接入阶段 90% 报错的来源(详见常见问题篇)。
qiankun 完整接入流程
qiankun = single-spa(调度) + import-html-entry(HTML Entry 加载) + Proxy 沙箱(隔离)。下面是一套最小可运行的主 / 子应用接入示例。
主应用(基座)
js
// main/src/micro.js
import { registerMicroApps, start } from "qiankun";
registerMicroApps(
[
{
name: "vue-app", // 子应用唯一名称,需与子应用 package.json name 一致
entry: "//localhost:7100", // 子应用 HTML 入口
container: "#subapp-container", // 挂载点
activeRule: "/vue", // 路由匹配规则,命中时加载该子应用
props: { token: "xxx" }, // 传给子应用的数据
},
{
name: "react-app",
entry: "//localhost:7200",
container: "#subapp-container",
activeRule: "/react",
},
],
{
beforeLoad: [(app) => console.log("before load", app.name)],
afterMount: [(app) => console.log("mounted", app.name)],
}
);
start({ prefetch: true, sandbox: { strictStyleIsolation: false } });子应用改造(以 Vue3 为例)
js
// sub/src/main.js
import { createApp } from "vue";
import App from "./App.vue";
let app = null;
// 独立运行时直接渲染
function render(props = {}) {
const { container } = props;
app = createApp(App);
app.mount(container ? container.querySelector("#app") : "#app");
}
if (!window.__POWERED_BY_QIANKUN__) render();
// ① 导出生命周期钩子
export async function bootstrap() {}
export async function mount(props) {
render(props); // props 里含主应用传来的数据与通信方法
}
export async function unmount() {
app.unmount();
app = null;
}三处必改配置
- 打包格式:
output.library设为应用名、libraryTarget: 'umd',让 qiankun 能拿到生命周期钩子 - publicPath:运行时用
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__动态修正,否则静态资源 404 - 跨域:子应用 dev-server 与静态资源需开启 CORS(
Access-Control-Allow-Origin),否则 HTML Entry 拉取失败
HTML Entry 加载原理
INFO
qiankun 采用 HTML Entry(相比 single-spa 的 JS Entry):直接请求子应用的 index.html,用 import-html-entry 解析出其中的 JS / CSS,将 HTML 模板注入容器、把 JS 放进沙箱执行。好处是子应用改动小、资源信息完整、便于样式隔离。
子应用生命周期
qiankun 沿用 single-spa 的生命周期模型,子应用必须导出三个(可选四个)钩子:
| 钩子 | 触发时机 | 典型工作 |
|---|---|---|
| bootstrap | 子应用首次加载时(仅一次) | 初始化全局资源、缓存 |
| mount | 每次进入子应用路由时 | 创建实例、挂载 DOM、订阅通信 |
| unmount | 每次离开子应用路由时 | 卸载实例、清理副作用、解绑事件 |
| update(可选) | 主应用主动更新 props 时 | 响应 props 变化 |
text
[路由命中 activeRule]
主应用 → qiankun → 子应用:
1. qiankun 请求子应用 index.html (HTML Entry)
2. qiankun 调用 bootstrap() (首次加载,仅一次)
3. qiankun 调用 mount(props) (挂载)
4. 子应用渲染完成 → 通知主应用
[路由离开]
5. qiankun 调用 unmount() (卸载清理)内存泄漏高发区
mount 里注册的定时器、事件监听、全局订阅,必须在 unmount 里逐一清理。否则反复切换子应用会累积泄漏,这是 qiankun 项目最常见的隐患。
JS 沙箱三种实现
沙箱的作用
沙箱用于隔离子应用对 window 的污染。子应用运行期间对全局变量的增删改,都应被限制在自己的作用域内,卸载后能干净还原,多个子应用之间也互不干扰。
qiankun 的沙箱演进经历了三个阶段:快照沙箱 → 单例 Proxy 沙箱 → 多例 Proxy 沙箱。
① 快照沙箱(SnapshotSandbox)
适用于不支持 Proxy 的低版本浏览器。原理:激活时给 window 拍快照,失活时对比差异并还原。
js
/**
* 快照沙箱
* 缺点:需遍历 window 所有属性,性能差、耗内存;且只支持单实例
*/
class SnapshotSandbox {
constructor() {
this.modifyPropsMap = {}; // 记录子应用修改过的变量
this.windowSnapshot = {}; // window 快照
}
active() {
// 1. 备份当前 window 快照
this.windowSnapshot = {};
for (const key in window) {
this.windowSnapshot[key] = window[key];
}
// 2. 恢复上次子应用修改的变量
for (const key in this.modifyPropsMap) {
window[key] = this.modifyPropsMap[key];
}
}
inactive() {
for (const key in window) {
// 对比快照,找出被修改的属性
if (window[key] !== this.windowSnapshot[key]) {
this.modifyPropsMap[key] = window[key]; // 记录,激活时恢复
window[key] = this.windowSnapshot[key]; // 还原 window
}
}
}
}
const sandbox = new SnapshotSandbox();
sandbox.active();
window.a = 1;
window.b = 2;
console.log(window.a, window.b); // 1 2
sandbox.inactive();
console.log(window.a, window.b); // undefined undefined
sandbox.active();
console.log(window.a, window.b); // 1 2② 单例 Proxy 沙箱(LegacySandbox)
用 Proxy 记录对 window 的新增 / 修改,失活时精准还原,无需遍历整个 window,性能优于快照沙箱。
js
/**
* 单例代理沙箱
* 缺点:仍直接操作真实 window,多个子应用同时运行会互相冲突
*/
class ProxySandbox {
constructor() {
this.addedPropsMap = new Map(); // 子应用新增的属性
this.modifyPropsMap = new Map(); // 子应用修改前的原始值
this.allPropsMap = new Map(); // 所有新增+修改,用于激活时恢复
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => window[key],
set: (target, key, value) => {
if (!window.hasOwnProperty(key)) {
this.addedPropsMap.set(key, value); // 新增
} else if (!this.modifyPropsMap.has(key)) {
this.modifyPropsMap.set(key, window[key]); // 记录原值
}
window[key] = value;
this.allPropsMap.set(key, value);
return true;
},
});
}
active() {
// 恢复子应用之前的新增和修改
this.allPropsMap.forEach((value, key) => (window[key] = value));
}
inactive() {
this.modifyPropsMap.forEach((value, key) => (window[key] = value)); // 还原修改
this.addedPropsMap.forEach((_, key) => delete window[key]); // 删除新增
}
}③ 多例 Proxy 沙箱(当前主力)
为什么需要多例
单例沙箱直接改真实 window,两个子应用同时运行会共用一个 window 而冲突。多例沙箱为每个子应用创建一个独立的 fakeWindow,读取时优先取自己的、找不到再回退到真实 window,写入只落在自己的 fakeWindow 上,从而实现真正的多实例隔离。
js
/**
* 多例代理沙箱
* 通过独立 fakeWindow 模拟操作,不污染真实 window
*/
class ProxySandbox {
constructor() {
this.running = false; // 沙箱是否激活
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => {
// 未激活:全部读真实 window;激活:优先读自己的
if (!this.running) return window[key];
return key in target ? target[key] : window[key];
},
set: (target, key, value) => {
if (this.running) target[key] = value; // 只写自己的 fakeWindow
return true;
},
});
}
active() {
this.running = true;
}
inactive() {
this.running = false;
}
}
const s1 = new ProxySandbox();
const s2 = new ProxySandbox();
s1.active();
s2.active();
s1.proxy.a = 1;
s2.proxy.a = 2;
console.log(s1.proxy.a, s2.proxy.a); // 1 2(互不影响)
console.log(window.a); // undefined(真实 window 未被污染)| 沙箱类型 | 隔离方式 | 多实例 | 性能 | 兼容性 |
|---|---|---|---|---|
| 快照沙箱 | 快照对比还原 | ❌ 单例 | 差(遍历 window) | 好(无需 Proxy) |
| 单例 Proxy | 记录增改,操作真实 window | ❌ 单例 | 较好 | 需 Proxy |
| 多例 Proxy | 独立 fakeWindow | ✅ 多例 | 好 | 需 Proxy |
CSS 样式隔离原理
子应用样式污染主应用 / 其它子应用,是微前端第二大隔离难题。qiankun 提供两种方案:
① Shadow DOM 严格隔离(strictStyleIsolation)
js
start({ sandbox: { strictStyleIsolation: true } });原理
把子应用的 DOM 挂载到一个 Shadow DOM 节点内。Shadow DOM 内的样式与外界完全隔离,是最彻底的方案。
副作用
Shadow DOM 会导致挂到 document.body 的元素(如 Ant Design 的 Modal、Message、Tooltip)样式丢失,因为它们逃出了 Shadow 边界。这类第三方 UI 库需额外配置 getPopupContainer 把弹窗挂回容器内。
② 运行时 CSS 作用域(experimentalStyleIsolation)
js
start({ sandbox: { experimentalStyleIsolation: true } });原理
qiankun 会给子应用的每条 CSS 规则动态添加属性选择器前缀,实现"软隔离":
css
/* 原始 */
.title {
color: red;
}
/* 改写后(div[data-qiankun="react-app"] 作用域) */
div[data-qiankun="react-app"] .title {
color: red;
}兼容性好、不影响弹窗,但对 @keyframes、@font-face、内联样式等覆盖不全,属于折中方案。
其它常见样式隔离手段
| 手段 | 原理 | 特点 |
|---|---|---|
| Shadow DOM | 原生影子边界 | 最彻底,但弹窗易失效 |
| CSS 前缀作用域 | 运行时改写选择器 | 兼容好,覆盖不全 |
| CSS Modules | 编译期生成 hash 类名 | 构建时隔离,需子应用配合 |
| BEM / 命名空间约定 | 人为加前缀 | 零成本,靠自觉,易失效 |
| 动态样式表增删 | mount 加载、unmount 移除 <style> | qiankun 默认行为,防止残留 |
加载 / 卸载时的样式处理
qiankun 在子应用 mount 时插入其 <style>,unmount 时移除,保证离开子应用后样式不残留污染主应用——这是"动态样式表"隔离的基础保障。
应用间通信机制
微前端拆分后,主子、子子之间需要共享登录态、用户信息、主题等。常见通信方式如下:
① props 下发(主 → 子)
主应用注册子应用时通过 props 传数据,子应用在 mount 生命周期里接收:
js
// 主应用
registerMicroApps([
{
name: "app1",
entry: "//localhost:7100",
container: "#c",
activeRule: "/app1",
props: { userInfo: { name: "张三" } },
},
]);
// 子应用
export async function mount(props) {
console.log(props.userInfo); // { name: "张三" }
}② qiankun 全局状态 GlobalState(双向)
INFO
qiankun 内置 initGlobalState 提供一个响应式的全局状态池,主子应用都能读写并监听变化,是 qiankun 推荐的通信方式。
js
// 主应用:初始化全局状态
import { initGlobalState } from "qiankun";
const actions = initGlobalState({ user: null, theme: "light" });
actions.onGlobalStateChange((state, prev) => console.log(state, prev));
actions.setGlobalState({ user: { name: "张三" } });
// 子应用:mount 时拿到 actions(挂在 props 上)
export async function mount(props) {
props.onGlobalStateChange((state) => console.log("状态更新", state));
props.setGlobalState({ theme: "dark" });
}③ 发布订阅 / EventBus(去中心化)
适用于任意框架,自实现一个全局事件总线挂到 window,主子应用共享:
js
class EventBus {
constructor() {
this.events = {};
}
on(name, fn) {
(this.events[name] ||= []).push(fn);
}
emit(name, data) {
(this.events[name] || []).forEach((fn) => fn(data));
}
off(name, fn) {
this.events[name] = (this.events[name] || []).filter((f) => f !== fn);
}
}
window.__BUS__ = new EventBus();通信方式对比
| 方式 | 方向 | 耦合度 | 适用场景 |
|---|---|---|---|
| props | 主 → 子 | 低 | 初始化数据下发 |
| GlobalState | 双向 | 中 | qiankun 全局共享态(登录/主题) |
| EventBus | 任意 | 低 | 跨应用事件、框架无关 |
| URL / 路由参数 | 任意 | 最低 | 简单参数、可分享的状态 |
| localStorage | 任意 | 低 | 持久化、跨标签页(注意隔离) |
通信原则
优先用低耦合的方式(props / URL)。全局状态虽方便,但滥用会让子应用间产生隐式依赖,退化成"分布式单体"。约定好数据契约、避免子应用直接读写彼此内部状态。
资源预加载与性能优化
微前端最大的性能痛点是切换子应用时的白屏。优化手段:
核心优化清单
- 预加载(prefetch):
start({ prefetch: true })在主应用空闲时提前拉取其它子应用资源 - 公共依赖共享:主子应用通过 externals / MF
shared复用同一份 React / Vue,避免重复加载 - 子应用保活(keep-alive):切换时不销毁而是隐藏,减少重复渲染开销
- 按需加载:子应用内部路由级懒加载,减小首屏包体积
- CDN + 强缓存:子应用静态资源上 CDN,合理设置缓存策略
- 骨架屏 / loading:切换过程展示占位,优化白屏体感
js
// 精细化预加载:仅预取指定子应用
import { prefetchApps } from "qiankun";
prefetchApps([{ name: "app1", entry: "//localhost:7100" }]);Module Federation 实战
除了 qiankun 这类"应用级"框架,Module Federation 提供了更细粒度的"模块级"共享,越来越多项目用它做微前端。
主应用(host)配置
js
// webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
// 远程应用别名 → 名称@远程入口地址
app1: "app1@http://localhost:3001/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true },
},
}),
],
};远程应用(remote)配置
js
new ModuleFederationPlugin({
name: "app1",
filename: "remoteEntry.js", // 供 host 加载的入口清单
exposes: {
"./Button": "./src/Button", // 暴露的模块
"./App": "./src/App",
},
shared: {
react: { singleton: true },
"react-dom": { singleton: true },
},
});主应用消费远程模块
jsx
import React, { Suspense } from "react";
const RemoteButton = React.lazy(() => import("app1/Button"));
export default function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<RemoteButton />
</Suspense>
);
}shared 的意义
singleton: true 保证共享库全局只有一个实例——这对 React 尤其重要(多份 React 会导致 Hooks 报错)。MF 会在运行时协商版本,优先复用已加载的兼容版本,避免重复下载。
MF 没有沙箱
Module Federation 不提供 JS / CSS 隔离。它擅长依赖共享与模块复用,但全局变量、样式冲突需自行处理。若既要模块共享又要隔离,可考虑 qiankun + MF 组合,或 Vite 生态的 vite-plugin-federation。
常见问题与踩坑
高频报错速查
以下是接入微前端时最常遇到的问题,按出现频率排序。
| 问题 | 原因 | 解决 |
|---|---|---|
| 子应用资源 404 | publicPath 用了相对路径 | 运行时设置 __webpack_public_path__ 为 qiankun 注入的地址 |
| 子应用 HTML 拉取失败(CORS) | 子应用未开启跨域 | dev-server 与生产资源配置 Access-Control-Allow-Origin |
| qiankun 找不到生命周期钩子 | 打包格式不对 | libraryTarget: 'umd' + 设置 library 为应用名 |
| 样式互相污染 | 未开启样式隔离 | 开 experimentalStyleIsolation 或用 Shadow DOM |
| Antd 弹窗样式丢失 | Shadow DOM 隔离了挂 body 的元素 | 配置 getPopupContainer 挂回容器内 |
| 路由冲突 / 404 | 子应用 base 未设置 | 子应用路由 base 设为 activeRule 前缀 |
| 切换子应用内存泄漏 | unmount 未清理副作用 | 定时器 / 监听 / 订阅在 unmount 中逐一清理 |
| 多个子应用全局变量冲突 | 用了单例沙箱 | 使用多例 Proxy 沙箱(qiankun 默认) |
| React 多实例 Hooks 报错 | 加载了多份 React | MF shared 设 singleton: true |
JS 沙箱面试高频总结
一句话回答
- 快照沙箱:拍照 → 改 → 对比还原,兼容老浏览器但性能差、只支持单例
- 单例 Proxy 沙箱:Proxy 记录增改并操作真实 window,性能好但多应用冲突
- 多例 Proxy 沙箱:每个子应用独立 fakeWindow,读回退、写隔离,支持多实例并存(qiankun 现役方案)
CSS 隔离面试高频总结
一句话回答
- Shadow DOM:原生影子边界最彻底,但挂 body 的弹窗会失效
- 运行时 CSS 前缀:给选择器加属性作用域,兼容好但覆盖不全
- 动态样式表:mount 插入 / unmount 移除
<style>,防止样式残留
结语
微前端的本质是用工程复杂度换取组织协作效率与技术演进自由。理解"加载 / 隔离 / 通信"三大核心问题,再看 qiankun、micro-app、无界如何各自取舍,就能在实际项目中做出合理的架构决策。