iframe作为微前端方案的几个问题

2 阅读5分钟

iframe提供了一个原生的window沙箱,内部有完整的historylocation接口,路由也彻底与主应用解耦。在微前端的实现中,天然能被用来隔离子应用。但iframe也存在以下几个问题:

  1. 路由状态丢失
  2. DOM割裂严重,弹窗只能在iframe内部展示
  3. iframe web应用之间通信困难
  4. iframe加载SPA应用白屏时间过长

下面,我们来逐个认识下他们,并看看有哪些办法来解决。

路由状态丢失

当浏览器刷新页面时,iframe的当前路由状态会丢失。比如,你在页面中使用了一个 <iframe>,iframe 内部加载了某个子页面。这个 iframe 的 URL 有一段路由路径:

http://your-domain.com/container
       └── iframe src="http://child-app.com/#/dashboard"

你在 iframe 内部导航到了一个profile页面,路由状态变成了

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 的状态,比如:

http://your-domain.com/container?childRoute=/profile

然后在加载 iframe 时拼接这个路由:

<iframe src={`http://child-app.com/#${childRoute}`}></iframe>
  • 你可能会问,iframe的路由状态怎么同步到父应用的URL上呢?

    我们可以重写掉iframe window上的pushState/replaceState方法,以及监听hashchange/popstate事件,把iframe的路由状态作为参数写到主应用的URL上。伪代码如下:

 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 内部调用代码,比如:

document.body.appendChild(modalElement);

弹窗只会出现在iframe内部,无法穿透iframe 显示在整个主页面(父页面)上方。你想要一个弹窗遮罩整个页面(全局),结果它只遮住了小小的 iframe 区域。

为什么无法覆盖?

iframe 是一个完整的独立页面(document) ,和父页面是两个完全分离的 DOM 世界。弹窗的定位和层级(如 position: fixedz-index)只能作用在当前 document,即iframe的document上。

解决办法

弹窗放在主应用,由子应用触发。子应用通过 postMessage 或全局事件总线发出事件或消息,主应用监听并渲染弹窗,弹窗 DOM 节点实际挂在主应用 DOM 上

// 子应用发消息
window.parent.postMessage({ type: 'OPEN_MODAL', data: {...} }, '*');

// 主应用监听
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发出消息:

iframe.contentWindow.postMessage('hello', 'https://iframe-domain.com');

被嵌入的 iframe 页面监听消息:

window.addEventListener('message', (event) => {
  // 安全校验
  if (event.origin === 'https://your-main-domain.com') {
    console.log(event.data);
  }
});

同源iframe与主页面的通信

主页面和同源iframe之间的通信是比较简单的,可以直接通过 JavaScript 访问彼此的对象和方法,没有跨域限制。

主页面访问iframe

<!-- 主页面 -->
<iframe id="myIframe" src="/child.html"></iframe>

主页面 JavaScript:

const iframe = document.getElementById('myIframe');

// 调用 iframe 中暴露的方法
iframe.onload = () => {
  iframe.contentWindow.sayHelloFromParent && iframe.contentWindow.sayHelloFromParent();
};

iframe 页面(/child.html):

<script>
  // 被主页面调用的方法
  function sayHelloFromParent() {
    alert('Hello from parent!');
  }
</script>

iframe调用主页面方法(向主页面发送消息) 主页面定义方法:

<script>
  window.sayHelloFromIframe = function () {
    alert('Hello from iframe!');
  };
</script>

iframe 页面中访问:

// 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 并加载目标页面,等用户点击或需要显示时再展示。示例:

<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来实现微前端的沙箱功能。看看它是如何来解决上述几个问题的。