为什么iframe页面在keep-alive中缓存无效?

Unknown Author 独酌
2025-12-25 前端

为什么iframe页面在keep-alive中缓存无效?切换tab总是会重新加载iframe中的页面?我觉得有必要深入分析一下,讲讲为什么?怎么解决?
【环境说明:vue3+vue-router4】

要想解决这个问题首先我们需要了解两个知识点:

  1. iframe什么情况下会重载?
  2. keep-alive工作原理是什么?

iframe什么情况下会重载?

触发加载有多种情况(前提设置有效的src):

  • html解析期的DOM树构建触发加载
  • iframe设置laoding="lazy"视口检测+渲染调用触发
  • iframe一些数据修改触发加载,例如:src、sandbox(但修改display/visibility属性不触发加载)
  • 浏览器行为触发加载,例如:用户点击、滚动、.resize()
  • DOM树更新触发加载

【DOM树更新触发加载】这个和我们主题相关,其加载触发的核心前提是【该iframe节点被纳入父页面DOM树】,底层对应DOM树的突变触发资源调度。

iframe这种DOM剥离会触发浏览器销毁对应的浏览上下文,导致iframe的运行状态彻底丢失。iframe挂载又是一个创建全新浏览上下文的过程,所以才会导致iframe重新加载。

keep-alive工作原理是什么?

我们知道keep-alive是用于存储被缓存的组件实例,这里我们用[Tab 形式路由 + keep-alive 的底层工作流程]为例,以「Tab1(路由 A)→ Tab2(路由 B)→ 切回 Tab1(路由 A)」的完整流程来讲讲它的工作逻辑:

  1. 首次加载 Tab1(路由 A):全新创建 + 挂载
    此时路由 A 组件未被缓存,Vue 会:
    创建路由 A 的组件实例(包含 data/ref 状态、方法等);
    根据组件实例生成全新 VNode 树(描述组件 DOM 结构的抽象对象);
    基于 VNode 树创建真实 DOM 元素,并将 DOM 挂载到页面的 Tab 容器中;
    路由 A 组件激活后,keep-alive 会将「组件实例 + VNode 树」存入缓存容器(内部 Map 结构)。

  2. 切换到 Tab2(路由 B):Tab1 失活(卸载 DOM + 保留缓存)
    此时切换路由,路由 A 组件进入失活状态:
    keep-alive 触发路由 A 的 deactivated 钩子;
    路由 A 对应的真实 DOM 会被从页面 DOM 树中卸载(移除),但 DOM 元素本身并未被销毁(只是脱离页面渲染树);
    路由 A 的「组件实例 + VNode 树」仍保存在 keep-alive 缓存中,状态(如表单输入、滚动位置)不会丢失;
    若 Tab2 是首次加载,会重复「首次加载」的流程(创建组件实例→生成 VNode→创建 DOM→挂载),若已缓存则直接复用(后续流程同 Tab1 切回逻辑)。

  3. 切回 Tab1(路由 A):复用缓存 + 挂载已有 DOM
    keep-alive 从缓存容器中直接取出路由 A 的「组件实例 + 已缓存的 VNode 树」(无需重新创建);
    基于缓存的 VNode 树,找到之前卸载的「真实 DOM 元素」(该 DOM 未被销毁,只是之前脱离了页面);
    将这份已有真实 DOM 直接重新挂载到页面 Tab 容器中(仅执行挂载操作,不执行 DOM 创建);
    触发路由 A 的 activated 钩子,组件激活完成,之前保留的状态(如表单内容)直接显示,无需重新渲染。

从上面完整的例子我们知道,keep-alive缓存的组件在失活状态DOM元素本身是脱离页面渲染树,激活重新挂载。

到这里我们已经找到keep-alive对iframe看似 “无效”的答案:keep-alive会让缓存组件脱离和重新挂载文档流,对iframe这种dom元素会触发重新加载。

vue-router也是一样,包裹起来也是会导致重新挂载问题

解决方案

通过以上的分析,我知道iframe想要“有缓存”,那就是要避免iframe脱离文档流,那我们就不能把iframe放在keep-alive或router-view里。这里以[Tab 形式路由 + keep-alive]的架构进行设置:

1. 创建项目

1
npm create vite@latest my-vue-project -- --template vue

2. 创建路由router.js

meta中keep-alive决定是否开启缓存, iframe决定是否页面为外链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { createRouter, createWebHistory } from 'vue-router'
import A from './components/A.vue'

const routes = [
{
path: '/',
redirect: '/a'
},
{
path: '/a',
name: 'A',
component: A,
meta: {
isKeepAlive: true,
title: '我是A页面, 被keep-alive缓存了'
}
},
{
path: '/b',
name: 'B',
component: A,
meta: {
isKeepAlive: false,
title: '我是B页面, 没有被缓存'
}
},
{
path: '/c',
name: 'C-cache(iframe)',
meta: { isKeepAlive: true, iframe: 'https://www.baidu.com', title: '我是C页面, iframe有“缓存”' }
},
{
path: '/d',
name: 'D-no-cache(iframe)',
meta: { isKeepAlive: false, iframe: 'https://baidu.com', title: '我是D页面, iframe没有“缓存”' }
}
]

const router = createRouter({
// 路由模式:createWebHistory(H5 历史模式,无 # 号),createWebHashHistory(哈希模式,带 # 号)
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes // 注入路由规则(简写:routes)
})

export default router

3. 实现App.vue用来加载路由

这里IframeComponent的src需要传入,不能通过在组件内使用useRoute获取,不然还是动态修改src导致外链重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template>
<div>
<div>
<router-link to="/a"><button :class="{active: route.name === 'A'}">A</button></router-link>
<router-link to="/b"><button :class="{active: route.name === 'B'}">B</button></router-link>
<router-link to="/c"><button :class="{active: route.name === 'C-cache(iframe)'}">C</button></router-link>
<router-link to="/d"><button :class="{active: route.name === 'D-no-cache(iframe)'}">D</button></router-link>
</div>

<router-view v-slot="{ Component, meta }">
<!-- keep-alive 缓存普通vue组件-->
<keep-alive>
<component :is="!route.meta?.iframe && route.meta.isKeepAlive ? Component : null"></component>
</keep-alive>
<!-- 非keep-alive组件(包含iframe非缓存) -->
<component :is="!route.meta.isKeepAlive ? (route.meta.iframe ? h(IframeComponent, { src: route.meta.iframe, name: route.name }) : Component) : null"></component>
</router-view>
<div v-show="route.meta?.iframe && route.meta.isKeepAlive">
<!-- 生成所有iframe路由, 并且通过v-show进行模拟keep-alive缓存数据 -->
<IframeComponent v-for="(i) in cacheIframe" :key="i.fullPath" v-show="route.fullPath === i.fullPath" :src="i.meta.iframe"></IframeComponent>
</div>
</div>
</template>
<script setup>
import { onUnmounted, h } from 'vue';
import { RouterView, useRoute, useRouter } from 'vue-router'
import { ref } from 'vue'
import IframeComponent from './components/IFrameComponent.vue';

const cacheIframe = ref([]);

const route = useRoute(); // 获取当前路由对象
const router = useRouter()

const removeGuard = router.beforeEach((to, from, next) => {
if (to.meta?.iframe && to.meta.isKeepAlive) {
const exists = cacheIframe.value.find(i => i.fullPath === to.fullPath)
if (!exists) {
cacheIframe.value.push(to)
}
}
next();
})

onUnmounted(() => {
if (typeof removeGuard === 'function') removeGuard()
})

</script>

<style scoped>
.active {
background-color: #42b983;
color: white;
}
</style>

4. A组件实现

承载普通页面容器组件

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute();
defineOptions({
name: 'A'
})
</script>

<template>
<h1>{{ route.meta.title }}</h1>
<input />
</template>

5. iframe组件

承载外链的容器组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { useRoute } from 'vue-router'
const props = defineProps({
src: {
type: String,
default: ''
}
})
const route = useRoute();
</script>
<template>
<h1>{{route.meta.title}}页</h1>
<iframe style="width: 800px;height:500px;" :src="props.src"></iframe>
</template>
标签:
独酌

独酌

小镇码农

相关文章