微前端与模块联邦
单体前端应用一旦大到"一个团队改不动、一个发布拖垮所有人",就需要拆分。微前端把应用拆成多个可独立开发、独立部署的子应用,再由主应用组合成完整产品。
一、微前端概念与价值
| 价值 | 说明 |
|---|---|
| 独立开发 | 每个子应用由独立团队负责,技术栈自选 |
| 独立部署 | 子应用单独发布上线,互不阻塞 |
| 技术栈无关 | Vue 子应用、React 子应用可共存 |
| 渐进式改造 | 老系统可以逐步迁移,不必推倒重来 |
| 故障隔离 | 单个子应用崩溃不影响整体(需做好容错) |
适用场景:大型后台管理系统(多个业务域)、老项目渐进重构、多团队协作的超级应用。
text
主应用(基座)
├─ 子应用 A(React,商品域)
├─ 子应用 B(Vue,订单域)
└─ 子应用 C(老 jQuery 系统)
组合渲染成一个"单页应用"二、方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| iframe | 浏览器原生隔离 | 最简单、隔离最强 | 通信难、刷新丢失状态、体验割裂 |
| single-spa | 路由分发,生命周期管理 | 框架中立、成熟 | 只解决加载,不解决隔离 |
| qiankun | single-spa + 沙箱 + 样式隔离 | 开箱即用,国产生态好 | 需侵入改造,兼容老浏览器受限 |
| Module Federation | webpack 运行时共享模块 | 真正的代码共享,构建期解决 | 仅限 webpack 系 |
| Web Components | 浏览器自定义元素标准 | 技术栈无关、标准 | 生态与 SSR 支持弱 |
三、qiankun 原理
qiankun 基于 single-spa 做应用加载,自己补上了沙箱隔离和样式隔离。
3.1 主应用接入
javascript
// 主应用注册子应用
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
{
name: "app-vue", // 唯一名称
entry: "//localhost:8081", // 子应用入口
container: "#subapp-container", // 挂载容器
activeRule: "/app-vue", // 匹配路由
},
{
name: "app-react",
entry: "//localhost:8082",
container: "#subapp-container",
activeRule: "/app-react",
},
]);
start(); // 启动3.2 子应用暴露生命周期
javascript
// 子应用(Vue)main.js 中导出生命周期钩子
export async function bootstrap() { /* 只执行一次:初始化 */ }
export async function mount(props) { /* 激活时:挂载渲染 */ }
export async function unmount() { /* 离开时:销毁清理 */ }
// 独立运行时也保持可用
if (window.__POWERED_BY_QIANKUN__) { /* 微前端环境 */ }
else { createApp(App).mount("#app"); }| 生命周期 | 触发时机 |
|---|---|
bootstrap | 子应用首次加载,只执行一次 |
mount | 每次激活(进入路由)时挂载 |
unmount | 每次失活(离开路由)时卸载销毁 |
四、JS 沙箱
子应用之间共享全局 window,会互相污染(同名变量、事件监听泄漏)。qiankun 提供两级沙箱:
4.1 快照沙箱(老浏览器)
javascript
// 原理:挂载前把 window 状态拍快照,卸载时恢复
class SnapshotSandbox {
constructor() {
this.modifyMap = {};
}
active() {
this.windowSnapshot = {};
for (const key in window) this.windowSnapshot[key] = window[key]; // 拍快照
Object.keys(this.modifyMap).forEach((k) => (window[k] = this.modifyMap[k]));
}
inactive() {
for (const key in window) {
if (window[key] !== this.windowSnapshot[key]) {
this.modifyMap[key] = window[key]; // 记录改动
window[key] = this.windowSnapshot[key]; // 恢复快照
}
}
}
}4.2 Proxy 沙箱(现代浏览器)
用 Proxy 代理 window:子应用读不到对方改动过的属性,自己写的属性落在"自己的虚拟 window"上。
javascript
const fakeWindow = Object.create(null);
const sandboxWindow = new Proxy(fakeWindow, {
get(target, key) {
return target[key] ?? window[key]; // 先查私有,再查真实 window
},
set(target, key, value) {
target[key] = value; // 写进私有 fakeWindow,不污染全局
return true;
},
});| 沙箱类型 | 隔离方式 | 性能 | 兼容性 |
|---|---|---|---|
| 快照沙箱 | 挂载/卸载时恢复 window | 每次切换遍历 window | IE 可用 |
| Proxy 沙箱 | 拦截读写,私有虚拟 window | 好 | 需要 Proxy |
五、样式隔离
5.1 qiankun 的样式隔离
激活子应用时,qiankun 会把子应用插入的 <style> 记录起来,失活时移除;同时支持把主应用样式与子应用隔离。
5.2 通用方案
| 方案 | 做法 | 特点 |
|---|---|---|
| CSS 命名空间 | 所有类名加前缀(如 .appA-btn) | 简单但靠自觉 |
| CSS Modules / scoped | 编译期哈希类名 | 子应用内部天然隔离 |
| Shadow DOM | 样式彻底隔离在 shadow root 内 | 隔离最强,但事件/弹层有坑 |
| 动态样式回收 | 失活时移除子应用 style 标签 | 框架自动处理 |
javascript
// Shadow DOM 示意:宿主元素挂一个 shadow root,样式互不影响
const host = document.getElementById("micro-app");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `<style>p { color: red; }</style><p>隔离的内容</p>`;注意:弹层、下拉等渲染到 body 的元素会逃出 shadow root,这是 Shadow DOM 隔离的主要代价。
六、Module Federation
webpack 5 提供的模块联邦:多个构建产物在运行时互相加载彼此的模块,真正做到代码共享。
6.1 概念
| 概念 | 说明 |
|---|---|
| host(宿主) | 运行时消费远程模块的应用 |
| remote(远程) | 暴露模块供别人使用的应用 |
| 远程模块 | exposes 出来的组件/库 |
| 共享依赖 | shared 指定只加载一份(如 React 只装一次) |
javascript
// 远程应用(remote)暴露模块
// webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "remoteApp",
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/components/Button",
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } },
}),
],
};javascript
// 宿主应用(host)动态加载远程模块
// React 侧用法
const RemoteButton = React.lazy(() => import("remoteApp/Button"));
<Suspense fallback="加载远程组件…">
<RemoteButton />
</Suspense>;6.2 联邦 vs 微前端
| 对比 | qiankun(运行时应用级) | Module Federation(构建期模块级) |
|---|---|---|
| 粒度 | 整个应用 | 任意模块 |
| 隔离 | 沙箱隔离 | 共享同一运行时(需 shared) |
| 通信 | props / 全局事件 | 模块间直接 import |
| 依赖 | 可各带各的 | shared 机制避免重复加载 |
两者可以结合:联邦解决"依赖共享与模块复用",qiankun 解决"应用隔离与生命周期"。
七、微前端通信
| 方式 | 说明 | 适用 |
|---|---|---|
| props | qiankun mount 时传入 { name, token } | 主 → 子,初始化数据 |
| 全局事件 | window.dispatchEvent(new CustomEvent(...)) | 任意方向,解耦 |
| 状态库共享 | 把 store 实例挂到全局 | 复杂状态,慎用 |
| 路由参数 | 通过 URL 传递 | 页面级参数 |
javascript
// qiankun 主应用传 props
start({ props: { user: { name: "admin", token: "xxx" } } });
// 子应用 mount 中接收
export async function mount(props) {
console.log(props.user.name);
window.addEventListener("app-login", (e) => console.log(e.detail)); // 监听全局事件
}
// 任意子应用广播事件
window.dispatchEvent(new CustomEvent("app-login", { detail: { uid: 1 } }));八、适用场景与权衡
| 维度 | 适合微前端 | 不适合 |
|---|---|---|
| 团队 | 多团队并行、独立发布 | 单团队小项目 |
| 规模 | 巨型应用、多业务域 | 中小应用(拆分即开销) |
| 改造 | 老系统渐进迁移 | 全新绿地项目 |
| 成本 | 接受复杂度与性能损耗 | 追求极致性能 |
主要代价:架构复杂度上升(沙箱、通信、联调)、首屏性能下降(多入口加载)、排障链路变长。微前端不是银弹,小项目直接用单一应用更划算。
九、要点速查
| 概念 | 一句话记忆 |
|---|---|
| 微前端 | 子应用独立开发部署,主应用组合 |
| qiankun | 生命周期管理 + JS 沙箱 + 样式隔离 |
| 快照沙箱 | 挂载拍快照,卸载恢复 |
| Proxy 沙箱 | 拦截 window 读写,虚拟私有 window |
| 样式隔离 | 命名空间 / scoped / Shadow DOM / 动态回收 |
| Module Federation | 构建期暴露模块,运行时共享 |
| 通信 | props、全局自定义事件、共享 store |
| 权衡 | 复杂度与性能换独立部署 |