iframe
iframe能够帮助我们嵌入更为丰富的视图内容,譬如VSCode这样的IDE也是典型的微前端框架,他使用了Electron作为底层,并且使用webview标签作为视图的容器。而在浏览器中我们往往使用iframe来加载不同域的内容。
iframe可以创建一个全新的独立的宿主环境,iframe的页面和父页面是分开的,作为独立区域而不受父页面的CSS或者全局的JavaScript影响。iframe的不足或缺陷也非常明显,其会进行资源的重复加载,占用额外的内存;其会阻塞主页面的onload事件,和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
iframe的改造门槛较低,但是从功能需求的角度看,其无法提供SEO,并且需要我们自定义应用管理与应用通讯机制。iframe的应用管理不仅要关注其加载与生命周期,还需要考虑到浏览器缩放等场景下的界面重适配问题,以提供用户一致的交互体验;这里我们再简要讨论下同源场景中的跨界面通讯解决方案。
📖 详细解读参阅 DOM CheatSheet
BroadcastChannel
BroadcastChannel能够用于同源不同页面之间完成通信的功能。它与window.postMessage的区别就是,BroadcastChannel只能用于同源的页面之间进行通信,而window.postMessage却可以用于任何的页面之间;BroadcastChannel可以认为是window.postMessage的一个实例,它承担了window.postMessage的一个方面的功能。
const channel = new BroadcastChannel("channel-name");
channel.postMessage("some message");
channel.postMessage({ key: "value" });
channel.onmessage = function (e) {
const message = e.data;
};
channel.close();
SharedWorker API
Shared Worker类似于Web Workers,不过其会被来自同源的不同浏览上下文间共享,因此也可以用作消息的中转站。
const worker = new SharedWorker("shared-worker.js");
worker.port.postMessage("some message");
worker.port.onmessage = function (e) {
const message = e.data;
};
const connections = [];
onconnect = function (e) {
const port = e.ports[0];
connections.push(port);
};
onmessage = function (e) {
connections.forEach(function (connection) {
if (connection !== port) {
connection.postMessage(e.data);
}
});
};
Local Storage
localStorage是常见的持久化同源存储机制,其会在内容变化时触发事件,也就可以用作同源界面的数据通信。
localStorage.setItem("key", "value");
window.onstorage = function (e) {
const message = e.newValue;
};
线程独立性问题
在Electron中的,webview天然地和外部的browserWindow拥有分开的线程和js上下文,甚至连devtools都要单独启用。因为它其实是Chromium中的Out-of-Process iframes的实现(经过了Electron方面的封装)。对应的概念就是Chrome App中的Webview组件。
iframe作为升级版的frame,一般来说都会被认为和上层的parent容器处在同一个进程中,因为基于html的spec,他们会拥有父容器的一个孩子BrowserContext。在这种情况下,iframe当中的js运行时便会阻塞外部的js运行,特别是当如果iframe中的代码质量不高而导致性能问题时,外层运行的容器会受到相当大的影响。这显然是我们不愿意看到的,因为webview中的内容仅仅会作为IDE拓展机制的一部分,我们不希望看到我们的外部UI和程序被iframe阻塞从而导致性能表现不佳。
幸运的是,Chromium在67版本之后默认开启了Site Isolation。基于它的描述,如果iframe中的域名和当前父域名不同(也就是大家熟悉的跨域情况),那么这个iframe中的渲染内容就会被放在两个不同的渲染进程中。而这就给我们解决线程独立性的问题带来了曙光。只需要将IDE主应用的页面挂在a.com的域名,而同时将iframe的的页面挂在另外一个域名下,那么这个iframe的进程就和主进程分开了。在这种模型下,iframe和主进程仅仅能通过postMessage和message事件进行数据通讯。但是在上面的模型中,仍然有一点需要注意。基于Site Isolation的特性,同一个页面中如果有多个,拥有同一个域名的多个iframe之间是共享进程的,因此他们仍然会互相卡顿。如果某个业务场景需要一个更为独立的iframe进程,它必须和其他iframe拥有不同的域名。
iframe生命周期
持久化
对于需要持久的iframe元素,我们始终将它挂载在一个body根节点下的固定区域中。同时,为其指定一个观察目标,使用MutationObserver和ResizeObserver(Chrome 61之后支持)对这个目标进行观察,以便能即使知道这个目标在可视区域中所处的位置。最后,根据计算出的位置,将这个iframe盖在目标区域上,从而看起来就好像一直嵌在目标中一样。
Why Not Iframe
为什么不用iframe,这几乎是所有微前端方案第一个会被challenge的问题。但是大部分微前端方案又不约而同放弃了iframe方案,自然是有原因的,并不是为了 “炫技” 或者刻意追求 “特立独行”。如果不考虑体验问题,iframe几乎是最完美的微前端解决方案了。
iframe最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
- url不同步。浏览器刷新iframe url状态丢失、后退前进按钮无法使用。浏览器前进/后退问题:iframe页面刷新会重置(比如说从列表页跳转到详情页,然后刷新,会返回到列表页),因为浏览器的地址栏没有变化。
- iframe的页面跳转到其他页面出问题,比如两个iframe之间相互跳转,直接跳转会只在iframe范围内进行。
- 全局上下文完全隔离,内存变量不共享。iframe内外系统的通信、数据同步等需求,主应用的cookie要透传到根域名都不同的子应用中实现免登效果。
- 页面加载问题:影响主页面加载,阻塞onload事件,本身加载也很慢,页面缓存过多会导致电脑卡顿。(无法解决)
- 布局问题:iframe必须给一个指定的高度,否则会塌陷。
- 弹窗及遮罩层问题:只能在iframe范围内垂直水平居中,没法在整个页面垂直水平居中。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了iframe方案。