📌进来聊一聊iframe在vue中的状态保持

3,784 阅读5分钟

在微前端概念盛行的当下,同时业内也有较为成熟的微前端框架,比如qiankun,再谈iframe可能很多人会对它口诛笔伐,但是基于它天然的数据隔离、无痛接入子应用的优点,iframe可能仍是我们在某些场景下的最佳选择。

保持iframe页面状态

在一个典型的平台应用中,父应用需要挂载多个子应用窗口,每个子应用都是一个iframe,来回切换窗口时,需要保持每个窗口的状态,如输入信息、锚点信息。

已知在vue中可以使用keep-alive缓存组件,于是采用它来缓存iframe,结果发现在切换窗口时iframe状态被重置了。

这里需要重新仔细阅读下官方文档:keep-alive。文中指出,keep-alive缓存的是vue组件实例,即js对象,而不是真实dom。它涉及到的2个生命周期activateddeactivated,分别执行dom的移除与插入操作。而当iframe挂载到dom中时,会自动加载其src指向的资源链接,并重置内部的状态,因此keep-alive无法保留iframe内部状态,无法缓存iframe。

image.png

那如何在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>

image.png

从打印来看,1次dom的移动操作,记录下了2次dom变动。第1次是节点c的remove,第2次是节点c的add。因此,使用此api会使得iframe重新加载,重置内部状态。

再回头看看LRU缓存时,组件更新,触发diff逻辑。

image.png

由于组件新旧节点首尾各不相同,则根据通过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后执行。

  1. 先删除节点A:新旧节点尾尾相同,BCD全部复用,A在新节点中不存在则移除A。 image.png
  2. 再新增节点D:新旧节点头头相同,BCD全部复用,E在旧节点中不存在则创建元素E插入到最后。 image.png

如此便避免了iframe的移动,实现了缓存。

以上场景会在vue2.x内发生,vue3 diff已优化,可以使用同步代码执行操作,不会再使iframe重新加载,放心使用,无心智负担。

👇👇戳一戳快速体验:

最后

Github上关于这类问题也提了不少issue,👉 issue #4362速览。

值得一提的是,<img>与iframe不同。在创建img dom时src就会请求资源,此场景下的img是复用的,它不会因为移动dom位置重新请求资源。而iframe是需要插入到dom树上时才会请求资源。

蓝橙色宇宙宇航员地球卡通互联网宣传中文动态引导关注.gif