Vue 中对跨域的 iframe 进行缓存

在 vue 应用程序中,有时候需要使用 iframe 嵌入其它网站的内容,并希望可以缓存 iframe 的状态。但如果被链接的源(origin) 与当前的 origin 不同时,iframe 就会发生跨域加载,这个时候,就不能在当前程序中获取 iframe 中的内容,包括 src,从而无法获取 iframe 中的状态,导致无法在初始化 iframe 时对状态进行恢复。

可以按以下方法来实现对 iframe 的缓存:

尝试过的方法

  1. 使用 keep-alive

    KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。

  2. 保存 iframe 实例,当组件挂载后添加到对应位置

    失败的原因与第 1 点相同,当其重新插入到页面时会重新加载。

解决方案

实现步骤

思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。

  1. 在路由中,将 component 赋值为一个空组件 index.vue
1
2
3
4
5
{
path: "/chathub",
name: "chathub",
component: ()=>import("@/chathub/index.vue")
},

index.js 代码:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<template>
<div class="iframe-example-index row justify-center">
<div class="column justify-center text-h6 text-secondary">加载中...</div>
</div>
</template>

<script>
import { onMounted, ref } from 'vue'
import store from '@/store/index'
import { useRoute } from '@/compositions/vueRouter'

export default {
name: "iframeContainer",
setup() {
const src = ref("www.baidu.com")

function getIframeElement() {
const iframeElement = document.getElementById('iframe-example-body')
return iframeElement
}

async function restoreSrc() {
const iframeElement = getIframeElement()
if (!iframeElement) return

// 设置值
iframeElement.setAttribute('src', src.value)
}

function initIFrame(force = false) {
if (!force && iframeSetting.initialized) return

console.log('开始创建 iframe')

// 初始化 iframe
const iframeTemp = document.createElement('iframe')
iframeTemp.setAttribute('id', 'iframe-example-body')
iframeTemp.setAttribute('width', '100%')
iframeTemp.setAttribute('height', '100%')
const container = document.getElementById('iframe-example-container')
// 判断是否有 children,有的话,移除
if (container.children.length > 0) {
container.removeChild(container.children[0])
}
container.appendChild(iframeTemp)
iframeTemp.setAttribute('src', src.value)
}

onMounted(async () => {
// 修改 iframe
getIframeElement()

// 创建一个 iframe,保存到 iframe-example-container children 中
initIFrame()

// 从 url 中获取 src
await restoreSrc()
})

return { src }
},
}
</script>

.iframe-example-index {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
}
  1. 在 router-view 处,加载一个 container 容器组件,通过 v-ifv-show 来控制显示隐藏
1
2
3
4
5
6
7
<Container v-if="iframeVisited" v-show="isIFramePageVisible"></Container>

<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>

container.js 代码:

1
2
3
4
<template>
<div id="iframe-example-container">
</div>
</template>
  1. 通过监听路由path的变化,改变 ChatHub 的显/隐,间接控制 iframe 的显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const isIFramePageVisible = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const iframeVisited = ref(false)

watch(
() => routes.path,
(value) => {
if (value === '/iframe') {
iframeVisited.value = true
isIFramePageVisible.value = true
} else {
isIFramePageVisible.value = false
}
},
{
immediate: true
}
)

注意事项

iframe 只有处于显示时,才会加载 iframe 里面的内容。因此,必须保证 iframe 不在后台实例化,可以通过以下方式实现:

  1. 通过 v-if 来控制初始化加载
  2. index.vue 第一次加载时,使用 js 初始化 iframe,然后添加到 ChatHub 作为其子元素。

如果后台先实例了 iframe 后,会导致从其它页面切换到 /chathub 页面时,无法正常加载 iframe 里面的内容,并影响性能。

参考

本文参考以下文章,在此报以诚挚谢意!

  1. vue缓存iframe的解决方法

  2. Vue KeepAlive 为什么不能缓存 iframe