1.前言
微前端架构是为了在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
微前端框架内的各个应用都支持独立开发部署、不限技术框架、支持独立运行、应用状态隔离但也可共享等特征。
从框架的应用隔离实现方案、实战、优缺点三个方面探一探各个框架
2.iframe
在没有各大微前端解决方案之前,iframe是解决这类问题的不二之选,因为iframe提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。
但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题:
- 1、url 不同步,浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- 2、UI 不同步,DOM 结构不共享,弹窗只能在iframe内部展示,无法覆盖全局
- 3、全局上下文完全隔离,内存变量不共享,iframe 内外系统的通信、数据同步等需求,主应用的 cookie
- 4、慢, 每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
3.iframe与父网页通信
同源 iframe 可以无限制通信,不同源 iframe 只能通过 postMessage 进行有限通信,且受同源策略严格限制。
在微前端、第三方小部件集成、支付网关等场景中,postMessage是跨域通信的标准解决方案
| 通信方式 | 同源支持 | 跨域支持 | 特点 |
|---|---|---|---|
| contentWindow | ✅ | ❌ | 直接访问 |
| contentDocument | ✅ | ❌ | 直接访问 |
| postMessage | ✅ | ✅ | 安全,异步 |
| MessageChannel | ✅ | ✅ | 双向通道 |
| BroadcastChannel | ✅ | ✅ | 广播通信 |
| localStorage | ✅ | ❌ | 存储共享 |
一. 同源 iframe 通信(完全访问)
- 父页面 → 子 iframe
<!-- 父页面 parent.html -->
<iframe id="myIframe" src="child.html"></iframe>
<script>
// 等待 iframe 加载完成
document.getElementById('myIframe').onload = function() {
const iframeWindow = this.contentWindow; // 获取 iframe 的 window
const iframeDocument = this.contentDocument; // 获取 iframe 的 document
// 1. 直接调用 iframe 中的函数
iframeWindow.childFunction('来自父页面的消息');
// 2. 直接修改 iframe 的 DOM
iframeDocument.getElementById('child-element').style.color = 'red';
// 3. 直接访问 iframe 的变量
console.log('子页面变量:', iframeWindow.childVariable);
// 4. 通过事件传递
iframeWindow.dispatchEvent(new CustomEvent('parent-message', {
detail: { message: 'Hello from parent' }
}));
};
</script>
- 子 iframe → 父页面
<!-- 子页面 child.html -->
<script>
// 1. 直接访问父页面
window.parent.parentFunction('来自子页面的消息');
// 2. 访问顶级窗口
window.top.topLevelFunction();
// 3. 修改父页面 DOM
window.parent.document.getElementById('parent-element').innerText = '已修改';
// 4. 通过事件传递
window.parent.dispatchEvent(new CustomEvent('child-message', {
detail: { message: 'Hello from child' }
}));
// 5. 通过 opener(如果是从父页面打开的)
if (window.opener) {
window.opener.receiveFromChild('通过opener通信');
}
</script>
二. 跨域 iframe 通信(受限制)
- 使用 postMessage(最安全)
<!-- 父页面 parent.html -->
<iframe id="crossOriginIframe" src="https://other-domain.com/child.html"></iframe>
<script>
const iframe = document.getElementById('crossOriginIframe');
// 1. 父页面发送消息到子 iframe
iframe.onload = function() {
// 发送消息到 iframe
iframe.contentWindow.postMessage(
{
type: 'greeting',
data: 'Hello from parent',
timestamp: Date.now()
},
'https://other-domain.com' // 目标源,可以是 '*' 但建议指定
);
};
// 2. 父页面接收来自子 iframe 的消息
window.addEventListener('message', function(event) {
// 安全检查:验证消息来源
if (event.origin !== 'https://other-domain.com') {
return; // 拒绝处理来自非预期源的消息
}
console.log('收到消息:', event.data);
console.log('来源:', event.origin);
console.log('来源窗口:', event.source);
if (event.data.type === 'response') {
console.log('处理响应:', event.data.message);
}
}, false);
</script>
<!-- 子页面 child.html (在 other-domain.com) -->
<script>
// 1. 子 iframe 接收来自父页面的消息
window.addEventListener('message', function(event) {
// 重要:验证消息来源
if (event.origin !== 'https://parent-domain.com') {
return; // 拒绝非信任源的消息
}
console.log('子页面收到:', event.data);
// 2. 子 iframe 发送响应回父页面
event.source.postMessage(
{
type: 'response',
message: 'Hello from child',
originalMessage: event.data
},
event.origin // 发送回消息来源
);
}, false);
// 3. 主动发送消息到父页面
function sendToParent() {
window.parent.postMessage(
{
type: 'notification',
message: '主动发送的消息'
},
'https://parent-domain.com' // 目标源
);
}
</script>
三. 高级通信模式
- 使用 MessageChannel(双向通道)
// 父页面
const iframe = document.getElementById('myIframe');
const channel = new MessageChannel();
// 监听端口1的消息
channel.port1.onmessage = function(event) {
console.log('父页面收到:', event.data);
// 回复消息
if (event.data.type === 'request') {
channel.port1.postMessage({
type: 'response',
data: 'Here is your data'
});
}
};
// 将端口2传递给 iframe
iframe.onload = function() {
iframe.contentWindow.postMessage(
{ type: 'INIT_PORT', port: channel.port2 },
'*',
[channel.port2] // 转移端口所有权
);
};
// 子 iframe
let messagePort;
window.addEventListener('message', function(event) {
if (event.data.type === 'INIT_PORT') {
messagePort = event.ports[0];
messagePort.onmessage = function(e) {
console.log('子页面收到:', e.data);
};
// 开始通信
messagePort.postMessage({
type: 'request',
data: '我需要一些数据'
});
}
});
- 使用 BroadcastChannel(广播通信)
// 所有页面(父页面、子iframe、其他标签页)
const channel = new BroadcastChannel('app-channel');
// 发送消息
channel.postMessage({
type: 'user-update',
data: { userId: 123, name: '张三' }
});
// 接收消息
channel.onmessage = function(event) {
console.log('收到广播:', event.data);
if (event.data.type === 'user-update') {
updateUI(event.data.data);
}
};
4.single-spa
Single-spa 实现了一套生命周期,开发者需要在相应的时机自己去加载对应的子应用。 它做的事情就是注册子应用、监听 URL 变化,然后加载对应的子应用js,执行对应子应用的生命周期流程。
优点:
- 敏捷性 - 独立开发、独立部署,微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新;
- 技术栈无关,主框架不限制接入应用的技术栈,微应用具备完全自主权;
- 增量升级,在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略.
缺点:
- 需要自己去加载子应用
- 不支持 Javascript 沙箱隔离,需要自己去使用single-spa-leaked-globals之类的库去隔离
- 不支持css隔离,需要自己使用single-spa-css库或者postcss等去解决样式冲突问题
- 无法预加载
5.qiankun
阿里的qiankun 是一个基于 single-spa 的微前端实现库,孵化自蚂蚁金融,帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
实现方案
- single-spa是基于js-entry方案,而qiankun 是基于html-entry 及沙箱设计,使得微应用的接入 像使用 iframe 一样简单。
- 主应用监听路由,加载对应子应用的html,挂载到主应用的元素内,然后解析子应用的html,从中分析出css、js再去沙盒化后加载执行,最终将子应用的内容渲染出来。
- qiankun实现样式隔离有两种模式可供开发者选择:
- strictStyleIsolation: 这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
- experimentalStyleIsolation: 当 experimentalStyleIsolation 被设置为 true 时,qiankun 会改写子应用所添加的样式,会为所有样式规则增加一个特殊的选择器规则,来限定其影响范围
- qiankun实现js隔离,采用了两种沙箱,分别为基于Proxy实现的沙箱和快照沙箱,当浏览器不支持Proxy会降级为快照沙箱
优点
- html entry的接入方式,不需要自己写load方法,而是直接写子应用的访问链接就可以。
- 提供js沙箱
- 提供样式隔离,两种方式可选
- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
- 社区活跃
- umi 插件,提供了 @umijs/plugin-qiankun 供 umi 应用一键切换成微前端架构系统 除了最后一点拓展以外,微前端想要达到的效果都已经达到。
- 应用间通信简单,全局注入
- 路由保持,浏览器刷新、前进、后退,都可以作用到子应用
缺点
- 改造成本较大,从 webpack、代码、路由等等都要做一系列的适配
- 对 eval 的争议,eval函数的安全和性能是有一些争议的:MDN的eval介绍;
- 无法同时激活多个子应用,也不支持子应用保活
- 无法支持 vite 等 ESM 脚本运行
