iframe提供了一个原生的window沙箱,内部有完整的history
和location
接口,路由也彻底与主应用解耦。在微前端的实现中,天然能被用来隔离子应用。但iframe也存在以下几个问题:
- 路由状态丢失
- DOM割裂严重,弹窗只能在iframe内部展示
- iframe web应用之间通信困难
- iframe加载SPA应用白屏时间过长
下面,我们来逐个认识下他们,并看看有哪些办法来解决。
路由状态丢失
当浏览器刷新页面时,iframe的当前路由状态会丢失。比如,你在页面中使用了一个 <iframe>
,iframe 内部加载了某个子页面。这个 iframe 的 URL 有一段路由路径:
js
http://your-domain.com/container
└── iframe src="http://child-app.com/#/dashboard"
你在 iframe 内部导航到了一个profile页面,路由状态变成了
js
http://your-domain.com/container
└── iframe src="http://child-app.com/#/profile"
这时候你刷新整个浏览器页面(即父页面),iframe 又重新加载了它最初的 src URL,原来的路由状态(比如 /profile
)会丢失 ,iframe的路由状态又回到了http://child-app.com/#/dashboard
为什么路由状态会丢失?
iframe 加载的是一个子页面,它自己内部维护自己的路由状态。当你刷新父页面时,整个 DOM 被重建 ,包括 iframe 标签本身。所以它的 src
会重新被设置成你原先的值(比如 http://child-app.com/#/dashboard
),不会记得你用户之前在 iframe 里导航到了 /profile
解决办法
把 iframe 的路由状态同步到父应用的 URL,在iframe加载时同步回iframe。
让父页面记录 iframe 的状态,比如:
bash
http://your-domain.com/container?childRoute=/profile
然后在加载 iframe 时拼接这个路由:
html
<iframe src={`http://child-app.com/#${childRoute}`}></iframe>
-
你可能会问,iframe的路由状态怎么同步到父应用的URL上呢?
我们可以重写掉iframe window上的pushState/replaceState方法,以及监听hashchange/popstate事件,把iframe的路由状态作为参数写到主应用的URL上。伪代码如下:
js
const iframeWindow = iframe.contentWindow;
const history = iframeWindow.history;
history.pushState = function (data, title, url: string) {
syncUrlToWindow(iframeWindow); // syncUrlToWindow 负责把iframe路由写到主应用的URL上
}
history.replaceState = function (data, title, url: string) {
syncUrlToWindow(iframeWindow);
}
iframeWindow.addEventListener("hashchange", () => syncUrlToWindow(iframeWindow));
iframeWindow.addEventListener("popstate", () => {
syncUrlToWindow(iframeWindow);
});
DOM割裂严重,弹窗只能在iframe内部展示
每个iframe或子应用有自己独立的window,document,CSS样式表,JS作用域。当你在 iframe 内部调用代码,比如:
js
document.body.appendChild(modalElement);
弹窗只会出现在iframe内部,无法穿透iframe 显示在整个主页面(父页面)上方。你想要一个弹窗遮罩整个页面(全局),结果它只遮住了小小的 iframe 区域。
为什么无法覆盖?
iframe 是一个完整的独立页面(document) ,和父页面是两个完全分离的 DOM 世界。弹窗的定位和层级(如 position: fixed
、z-index
)只能作用在当前 document,即iframe的document上。
解决办法
弹窗放在主应用,由子应用触发。子应用通过 postMessage
或全局事件总线发出事件或消息,主应用监听并渲染弹窗,弹窗 DOM 节点实际挂在主应用 DOM 上
js
// 子应用发消息
window.parent.postMessage({ type: 'OPEN_MODAL', data: {...} }, '*');
js
// 主应用监听
window.addEventListener('message', (event) => {
if (event.data.type === 'OPEN_MODAL') {
showGlobalModal(event.data.data);
}
});
iframe web应用之间通信困难
浏览器出于安全目的实施了同源策略,同源是指协议、域名、端口都相同。当iframe的协议、域名和端口与主页面都相同时,我们把它叫做同源iframe,否则,叫做跨越iframe。
同源策略给iframe与主页面之间的通信带来了困难,尤其是跨域iframe。主页面无法直接访问跨域iframe页面的 DOM、JS 对象等,会抛出安全错误。
跨域iframe与主页面的通信
虽然困难,但不是不可能。我们可以使用HTML5提供的官方跨域通信机制postMessage API。
postMessage API
由主页面调用postMessage发出消息:
js
iframe.contentWindow.postMessage('hello', 'https://iframe-domain.com');
被嵌入的 iframe 页面监听消息:
js
window.addEventListener('message', (event) => {
// 安全校验
if (event.origin === 'https://your-main-domain.com') {
console.log(event.data);
}
});
同源iframe与主页面的通信
主页面和同源iframe之间的通信是比较简单的,可以直接通过 JavaScript 访问彼此的对象和方法,没有跨域限制。
主页面访问iframe
html
<!-- 主页面 -->
<iframe id="myIframe" src="/child.html"></iframe>
主页面 JavaScript:
ini
const iframe = document.getElementById('myIframe');
// 调用 iframe 中暴露的方法
iframe.onload = () => {
iframe.contentWindow.sayHelloFromParent && iframe.contentWindow.sayHelloFromParent();
};
iframe 页面(/child.html):
html
<script>
// 被主页面调用的方法
function sayHelloFromParent() {
alert('Hello from parent!');
}
</script>
iframe调用主页面方法(向主页面发送消息) 主页面定义方法:
html
<script>
window.sayHelloFromIframe = function () {
alert('Hello from iframe!');
};
</script>
iframe 页面中访问:
js
// iframe 中的 JS
window.parent.sayHelloFromIframe && window.parent.sayHelloFromIframe();
另外,
BroadcastChannel API
是一个浏览器提供的用于同源文档之间通信 的原生 Web API。它可以在不同的浏览器上下文环境之间进行消息广播 。它也能解决主页面与同源iframe之间的通信,具体请查阅Broadcast Channel API。
iframe加载SPA应用白屏时间过长
这个问题常见于基于 iframe
的微前端架构或嵌入式系统中。对于 SPA(Single Page Application)应用来说,iframe
每次加载都重新初始化一整个应用,导致白屏时间长、用户体验差。
下面是一些常见的优化方案,可以有效减少 iframe
白屏时间:
预加载 iframe
提前创建隐藏的 iframe
并加载目标页面,等用户点击或需要显示时再展示。示例:
ini
<iframe id="app-frame" src="about:blank" style="display: none;"></iframe>
<script>
const iframe = document.getElementById("app-frame");
iframe.src = "https://your-spa-app.com"; // 提前加载
iframe.onload = () => {
// 等加载完成后再展示
iframe.style.display = "block";
};
</script>
使用 Loading Skeleton / 占位动画
在 iframe 加载时展示骨架屏或 loading 动画,减少"白屏感知"。我们可以分两步来实现:
iframe
容器层级内加一层 loading 遮罩层;- 监听
iframe.onload
后移除遮罩。
使用 keep-alive iframe 或 sandbox 缓存技术
如果使用微前端框架(如 Qiankun、Wujie),可以启用 子应用保活 机制:
- 子应用第一次加载后,把frame相关的DOM保持在内存中;
- 后续切换时直接切回 DOM,不重新初始化。
总结
我们看完了iframe常见的4个问题,和他们的解决办法。如果感兴趣并想深入了解的,可以学习下Wujie这个微前端开源仓库,因为它采用了iframe来实现微前端的沙箱功能。看看它是如何来解决上述几个问题的。