在微前端概念盛行的当下,同时业内也有较为成熟的微前端框架,比如qiankun,再谈iframe可能很多人会对它口诛笔伐,但是基于它天然的数据隔离、无痛接入子应用的优点,iframe可能仍是我们在某些场景下的最佳选择。
保持iframe页面状态
在一个典型的平台应用中,父应用需要挂载多个子应用窗口,每个子应用都是一个iframe,来回切换窗口时,需要保持每个窗口的状态,如输入信息、锚点信息。
已知在vue中可以使用keep-alive缓存组件,于是采用它来缓存iframe,结果发现在切换窗口时iframe状态被重置了。
这里需要重新仔细阅读下官方文档:keep-alive。文中指出,keep-alive缓存的是vue组件实例,即js对象,而不是真实dom。它涉及到的2个生命周期activated与deactivated,分别执行dom的移除与插入操作。而当iframe挂载到dom中时,会自动加载其src指向的资源链接,并重置内部的状态,因此keep-alive无法保留iframe内部状态,无法缓存iframe。
那如何在vue中保持iframe状态呢?
我们可以利用css的display属性来实现。切换窗口时,给未激活的iframe设置display: none,给需要激活的窗口移除display属性。在display状态切换时,iframe不会重新加载,实现了保持状态的需求,这一点在vue中可以通过v-show指令快速实现。
清理iframe页面
iframe另一个被诟病的点是对内存的消耗,尤其是在需要缓存iframe应用的情况下,同时加载多个iframe,很快就会耗尽浏览器分配的内存直至应用崩溃,这不是产品乐意看见的。因此需要在合适的时间清理iframe应用,回收内存。
function clearIframe(id){
const iframe = document.getElementById(id);
const iframeWindow = iframe.contentWindow;
iframe.src = 'about:blank';
iframe.parentNode.removeChild(iframe);
iframe.remove();
el.close();
iframe = null;
}
实际应用中的vue中iframe缓存
在实际的一个一对多的客户服务应用中,在资源池达阈值时,基于LRU算法对缓存进行老化,将最久访问过的iframe页面移除,释放资源空间,同时追加新的iframe页面。通过v-for遍历iframe页面列表进行模板渲染,结合v-show方案实现iframe方案。
<template>
<div>
<div
class='iframe-wrapper'
v-for="item in list"
:key="item.id"
v-show="activeId === item.id"
>
<iframe src="item.url"></iframe>
</div>
</div>
</template>
<script>
export default {
name: 'iframe-component',
data(){
return {
list: [
{id: 1, url: 'https://www.baidu.com'},
{id: 2, url: 'https://www.jd.com'},
],
activeId: 1,
}
}
}
</script>
实际发现,当iframe个数未达到资源池阈值时,应用内存稳定上升可控,然而当资源池溢出,对最久的iframe页面进行老化,追加新的iframe时,发现页面中已缓存的多个iframe页面批量重新加载了,导致应用内存占用急剧上升,应用崩溃。
按理说,vue组件会在更新时会最大程度地复用节点,而且我们正确地使用了key属性。既然如此,有必要了解下vue中diff算法运行机制。
vue2.x diff算法
vue设计的diff算法对更新的组件的新旧节点进行比对,移动、删除、插入dom,最大程度地复用dom,提升vnode比对效率。
这里有一篇关于vue2.x diff算法的保熟文档值得一看:为什么 Vue 中不要用 index 作为 key?(diff 算法详解),因此本文不再过多赘述diff内运行机制。
值得一提的是,在移动DOM时,源码中使用的insertBefore在web环境中使用的是DOM的insertBefore api,该操作会先将需要移动的节点从dom中移除,再插入到dom中对应位置。
我们可以使用MutationObserver对象来观察dom insertBefore:
<div id="app">
<div id="a">a</div>
<div id="b">b</div>
<div id="c">c</div>
</div>
<script>
const app = document.querySelector('#app');
const adom = document.getElementById('a');
const bdom = document.getElementById('b');
const cdom = document.getElementById('c');
const observer = new MutationObserver(entries => {
console.log('entries:', entries);
})
observer.observe(app, {
childList: true,
})
function insert(){
app.insertBefore(cdom, adom);
}
setTimeout(insert, 1000)
</script>
从打印来看,1次dom的移动操作,记录下了2次dom变动。第1次是节点c的remove,第2次是节点c的add。因此,使用此api会使得iframe重新加载,重置内部状态。
再回头看看LRU缓存时,组件更新,触发diff逻辑。
由于组件新旧节点首尾各不相同,则根据通过map缓存的旧节点key信息,在旧节点中找到与新开始节点的sameNode,然后将目标dom移动到队头。依次循环直到BCDE都移动到对应位置,删除dom A,创建dom F插入队尾。
可见,一番操作下来BCDE都被移动位置插入到dom中,对于iframe而言,重新加载了嵌入的子应用,即使使用了display:none也无法保留状态。
既然如此,如何才能避免此场景下iframe重新加载呢?
仍是分析diff步骤,发现在不改变dom顺序的情况下可满足iframe的复用。可以将以上操作拆分为两步,先删除最久子应用,diff后更新好组件,再追加新的子应用,再次diff更新组件。
this.list.shift();
+ this.$nextTick(()=>{
this.list.push();
+ })
借助异步代码,将追加操作延迟到第1次操作更新好dom后执行。
- 先删除节点A:新旧节点尾尾相同,BCD全部复用,A在新节点中不存在则移除A。
- 再新增节点D:新旧节点头头相同,BCD全部复用,E在旧节点中不存在则创建元素E插入到最后。
如此便避免了iframe的移动,实现了缓存。
以上场景会在vue2.x内发生,vue3 diff已优化,可以使用同步代码执行操作,不会再使iframe重新加载,放心使用,无心智负担。
👇👇戳一戳快速体验:
最后
Github上关于这类问题也提了不少issue,👉 issue #4362速览。
值得一提的是,<img>与iframe不同。在创建img dom时src就会请求资源,此场景下的img是复用的,它不会因为移动dom位置重新请求资源。而iframe是需要插入到dom树上时才会请求资源。