Appearance
微前端
一、为什么需要微前端?
微前端是将不同的功能按照不同的维度拆分成多个子应用,通过主应用来加载这些子应用的技术架构。其核心思想在于"拆分"与"整合",通过分而治之的方式解决复杂前端应用的开发和管理问题。
微前端借鉴了这种分层和模块化的思想,将单一的大型前端应用拆分为多个可以独立开发、独立部署的子应用。
微前端解决的核心问题
- 技术栈异构:不同团队可以使用不同的技术栈同时开发一个应用
- 独立开发部署:每个团队开发的模块都可以独立开发、独立测试、独立部署
- 渐进式迁移:支持现有应用的逐步改造和迁移,降低技术升级风险
- 团队自治:各团队可以按照自己的节奏进行开发和发布
实现思路
通过将应用划分为若干个子应用,将子应用打包成独立的模块,在路由切换时动态加载不同的子应用。这种方式使得每个子应用都能保持技术独立性,同时又能整合为一个完整的用户体验。
二、微前端技术方案对比
关键技术考量
在选择微前端技术方案时,需要重点考虑三个核心问题:
- 应用拆分策略:如何将单体应用合理拆分为微前端架构
- 应用通信机制:子应用之间如何高效、可靠地进行数据交换
- 应用隔离方案:如何确保子应用之间的运行环境隔离
iframe 方案
实现原理:
- 通过 iframe 标签加载子应用
- 使用 postMessage API 进行应用间通信
- 利用浏览器原生的沙箱机制实现环境隔离
优势:
- 实现简单,技术成熟
- 天然的样式和脚本隔离
- 完美的沙箱安全机制
局限性:
- 用户体验较差(弹窗受限、路由状态丢失)
- 浏览器前进后退行为复杂
- 性能开销相对较大
Web Components 方案
实现原理:
- 基于自定义 HTML 元素构建子应用
- 通过 CustomEvent 实现组件间通信
- 利用 Shadow DOM 提供样式和作用域隔离
优势:
- 浏览器原生支持,无框架依赖
- 良好的组件封装性
- 天然的样式隔离机制
局限性:
- 浏览器兼容性要求较高
- 学习曲线相对陡峭
- 调试和样式定制相对困难
single-spa 方案
实现原理:
- 基于路由劫持实现应用的动态加载
- 使用 SystemJS 作为模块加载器
- 通过 props 属性进行应用间通信
- 需要自行实现 JS 和 CSS 沙箱机制
优势:
- 框架无关,技术栈灵活
- 社区生态相对成熟
- 支持渐进式迁移
局限性:
- 需要一定的学习成本
- 缺乏内置的沙箱机制
- 需要对现有应用进行改造适配
Module Federation 方案
实现原理:
- 基于 Webpack 5 的模块联邦特性
- 通过共享模块机制实现通信
- 模块级别的代码共享和复用
优势:
- 构建时优化,性能较好
- 模块共享机制完善
- 开发体验相对友好
局限性:
- 依赖 Webpack 5 构建工具
- 缺乏完整的沙箱隔离机制
- 配置相对复杂
三、微前端框架对比
qiankun
定位:基于 single-spa 的企业级微前端架构解决方案
背景:蚂蚁金服开源,在阿里内部有大规模落地实践
核心理念:应用级微前端,基于路由的应用加载和切换
micro-app
定位:基于 Web Components 的轻量级微前端框架
背景:京东零售开源,追求极简接入和原生体验
核心理念:组件式微前端,将子应用作为自定义 HTML 元素
无界
定位:基于 Web Components + iframe 的高性能微前端框架
背景:腾讯开源,专注于解决 iframe 的体验问题
核心理念:iframe 级隔离,Web Components 级体验
核心实现原理对比
| 框架 | 技术原理 | 沙箱机制 | 样式隔离 | 路由同步 |
|---|---|---|---|---|
| qiankun | 路由劫持 + JS Entry | JS Proxy 沙箱 | Shadow DOM / CSS Scoped | 手动配置 |
| micro-app | Web Components + Module Federation | 基于 Proxy 的沙箱 | Web Components 天然隔离 | 自动同步 |
| 无界 | iframe + Web Components | iframe 原生沙箱 | iframe 完全隔离 | 自动同步 |
应用加载方式
qiankun - JS Entry 模式
javascript
// 基于路由的应用注册
registerMicroApps([
{
name: "react-app",
entry: "//localhost:7100",
container: "#subapp-container",
activeRule: "/react",
},
]);micro-app - 组件式加载
html
<!-- 自定义元素方式 -->
<micro-app
name="app1"
url="http://localhost:3000"
baseroute="/app1"
></micro-app>无界 - iframe 增强
javascript
// 创建无界应用
Wujie.start({
name: "vue-app",
url: "http://localhost:8080",
el: "#container",
exec: true,
sync: true,
});1. 接入成本
| 维度 | qiankun | micro-app | 无界 |
|---|---|---|---|
| 子应用改造 | 需要生命周期改造 | 几乎零改造 | 完全零改造 |
| 配置复杂度 | 中等 | 简单 | 简单 |
| 学习成本 | 较高 | 较低 | 较低 |
| 接入时间 | 1-2 天 | 几小时 | 几小时 |
2. 隔离能力
JS 隔离
qiankun:⭐️⭐️⭐️⭐️⭐️
- 基于 Proxy 的完善沙箱
- 支持多实例同时运行
- 完善的错误边界
micro-app:⭐️⭐️⭐️⭐️
- 基于 Proxy 的沙箱
- 较好的作用域隔离
- 部分场景可能存在冲突
无界:⭐️⭐️⭐️⭐️⭐️
- iframe 级完全隔离
- 无任何 JS 冲突风险
- 最安全的隔离方案
CSS 隔离
qiankun:⭐️⭐️⭐️⭐️
- 支持 Shadow DOM 隔离
- 支持 CSS Scoped 方案
- 需要手动配置
micro-app:⭐️⭐️⭐️⭐️⭐️
- Web Components 天然隔离
- 自动样式作用域
- 无需额外配置
无界:⭐️⭐️⭐️⭐️⭐️
- iframe 完全样式隔离
- 零样式污染风险
- 最彻底的隔离方案
3. 通信机制
qiankun - 基于 props
javascript
// 主应用传递数据
registerMicroApps([
{
name: "app1",
// ...其他配置
props: {
userInfo: { name: "张三" },
onGlobalStateChange: () => {},
},
},
]);
// 子应用接收
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
// 状态变更处理
});
}micro-app - 基于 CustomEvent
javascript
// 主应用发送数据
microApp.setData("app1", { type: "数据更新", data: {} });
// 子应用监听
window.addEventListener("datachange", (e) => {
const data = e.detail.data;
});无界 - 基于事件总线
javascript
// 主应用通信
window.$wujie?.bus.$emit("event", data);
// 子应用通信
window.parent?.$wujie?.bus.$on("event", (data) => {
// 处理数据
});4. 路由管理
| 特性 | qiankun | micro-app | 无界 |
|---|---|---|---|
| 路由模式 | 路由劫持 | 自动同步 | 自动同步 |
| 路由同步 | 手动配置 | 自动处理 | 自动处理 |
| 路由冲突 | 可能发生 | 自动解决 | 自动解决 |
| 前进后退 | 完全支持 | 完全支持 | 完全支持 |
5. 性能表现
加载性能
qiankun:中等
- 需要加载框架代码
- 沙箱初始化开销
- 资源预加载支持
micro-app:较好
- 轻量级框架
- 原生 Web Components
- 模块联邦优化
无界:优秀
- 接近原生 iframe 性能
- 无额外框架开销
- 完善的预加载机制
运行时性能
qiankun:中等
- 沙箱代理有一定开销
- 多实例内存占用较高
micro-app:较好
- 接近原生性能
- 内存占用较低
无界:优秀
- iframe 级别性能
- 内存隔离,互不影响
团队技术能力
| 框架 | 技术要求 | 维护成本 | 扩展性 |
|---|---|---|---|
| qiankun | 较高,需要理解微前端原理 | 中等,有完善的最佳实践 | 优秀,支持复杂场景扩展 |
| micro-app | 较低,开箱即用 | 较低,配置简单 | 良好,满足大部分场景 |
| 无界 | 较低,简单易用 | 较低,稳定可靠 | 良好,专注于嵌入场景 |
长期维护考虑
- qiankun:蚂蚁金服持续维护,社区活跃,适合长期项目
- micro-app:京东零售团队维护,更新频繁,适合追求新技术
- 无界:腾讯团队维护,稳定性好,适合对稳定性要求高的项目
社区生态
| 指标 | qiankun | micro-app | 无界 |
|---|---|---|---|
| GitHub Stars | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
| 文档完善度 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ |
| 社区活跃度 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
| 企业使用案例 | ⭐️⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️⭐️ | ⭐️⭐️⭐️ |
核心结论:
- 求稳定、功能全 → 选择 qiankun
- 求快速、轻量级 → 选择 micro-app
- 求安全、零改造 → 选择 无界
四、JS 沙箱实现
沙箱 防止应用加载的时候 对 widow 造成污染
快照沙箱
js
/**
* 快照沙箱
* 缺点:性能较差浪费内存,因为需要遍历存储window对象的所有属性
*/
class SnapshotSandbox {
constructor() {
// 记录修改的变量
this.modifyPropsMap = {};
// window快照
this.windowSnapshot = {};
}
// 激活沙箱
active() {
// 激活沙箱备份window快照
this.windowSnapshot = {};
for (let key in window) {
this.windowSnapshot[key] = window[key];
}
// 恢复上次修改的变量
for (let key in this.modifyPropsMap) {
window[key] = this.modifyPropsMap[key];
}
}
// 失活沙箱
inactive() {
this.modifyPropsMap = {};
for (let key in window) {
// 由于在激活沙箱时备份了window快照
// 所以这里只需要**对比快照**和当前window的变量是否相等
if (window[key] != this.windowSnapshot[key]) {
// 记录修改的变量,激活的时候恢复挂载
this.modifyPropsMap[key] = window[key];
// 恢复window快照
window[key] = this.windowSnapshot[key];
}
}
}
}
const snapshotSandbox = new SnapshotSandbox();
snapshotSandbox.active(); // 激活沙箱
window.a = 1;
window.b = 2;
console.log(window.a, window.b); // 1 2
snapshotSandbox.inactive(); // 失活沙箱
console.log(window.a, window.b); // undefined undefined
snapshotSandbox.active(); // 激活沙箱
console.log(window.a, window.b); // 1 2代理沙箱
js
/**
* 代理沙箱
* 缺点:如果两个应用一起运行 window就一个,会导致数据冲突
*/
class ProxySandbox {
constructor() {
// 存储新增
this.addedPropsMap = new Map();
// 存储window修改之前的原始值
this.modifyPropsMap = new Map();
// 所有新增和修改的属性
this.allPropsMap = new Map();
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => {
return window[key];
},
set: (target, key, value) => {
if (!window.hasOwnProperty(key)) {
// 记录新增的值
this.addedPropsMap.set(key, value);
} else if (!this.modifyPropsMap.has(key)) {
// 记录window修改前的值
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.addedPropsMap.forEach((value, key) => {
delete window[key];
});
// 恢复修改的属性
this.modifyPropsMap.forEach((value, key) => {
window[key] = value;
});
}
}
let sandbox = new ProxySandbox();
sandbox.proxy.a = 1;
console.log(window.a, sandbox.proxy.a); // 1 1
sandbox.inactive(); // 失活沙箱
console.log(window.a, sandbox.proxy.a); // undefined undefined
sandbox.active(); // 激活沙箱
console.log(window.a, sandbox.proxy.a); // 1 1代理沙箱-多例模式
js
/**
* 代理沙箱-多例模式
* 通过代理对象模拟操作,不操作真实的window
*/
class ProxySandbox {
constructor() {
this.running = false; // 沙箱是否激活
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => {
if (!this.running) {
// 沙箱未激活时,不操作proxy上的值,全部从window上获取
return window[key];
}
return key in target ? target[key] : window[key];
},
set: (target, key, value) => {
if (this.running) {
// 沙箱激活时,修改proxy上的值
target[key] = value;
}
return true;
},
});
}
// 激活沙箱
active() {
this.running = true;
}
// 失活沙箱
inactive() {
this.running = false;
}
}
const sandbox1 = new ProxySandbox();
const sandbox2 = new ProxySandbox();
sandbox1.active(); // 激活沙箱
sandbox1.proxy.a = 1;
console.log(sandbox1.proxy.a); // 1
sandbox1.inactive(); // 失活沙箱
console.log(sandbox1.proxy.a); // undefined