最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:
javascript
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>
看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。
我们先了解下 KeepAlive
KeepAlive (熟悉的可跳过本节)
被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现"假卸载", 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated
和 deactivated
。
keepAlive 是需要渲染器支持的,在执行 mountComponent
时,如果发现是 __isKeepAlive
组件,那么会在上下文注入 move
方法。
javascript
function mountComponent(vnode, container, anchor) {
/**... */
const instance = {
/** ... */
state,
props: shallowReactive(props),
// KeepAlive 实例独有
keepAliveCtx: null
};
const isKeepAlive = vnode.__isKeepAlive;
if (isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
};
}
}
原因
通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件"销毁"时,会将真实 DOM 移动到"隐藏容器"中,组件重新"渲染"时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。
解决方案
思路 :路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show
改变显/隐。
-
在路由注册时,将 component 赋值为一个空组件
javascript{ path: "/chathub", name: "chathub", component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数 },
-
在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏
javascript<ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub> <router-view v-slot="{ Component }"> <keep-alive :include="keepAliveList"> <component :is="Component"></component> </keep-alive> </router-view>
-
监听路由的变化,改变 iframe 的显/隐
javascriptconst isChatHubPage = ref(false) // 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件 const chatHubVisited = ref(false) watch( () => routes.path, (value) => { if (value === '/chathub') { chatHubVisited.value = true isChatHubPage.value = true } else { isChatHubPage.value = false } }, { immediate: true } )
-
ChatHub.vue组件代码(有单个或者多个iframe情况)
javascript<template> <div class="iframe-container"> <iframe v-for="(item, index) in iframeList" v-show="showIframe(item, index)" :key="item.url" :src="item.url" frameborder="0" ></iframe> </div> </template> <script lang="ts" setup> export default { name: "ChatHub", }; import { ref, reactive } from "vue"; import { useRoute, useRouter } from "vue-router"; const route = useRoute(); const iframeList = reactive([ {name: 1, url: "https://xxx"}, {name: 2, url: "https://yyy"} ]) // 是否显示 const showIframe = (item, index) => { if (route.query.url === item.url) { return true; } else { return false; } }; </script>