项目背景

在我们日常项目开发中,可能会涉及到其他项目嵌入到本项目中。本人在工作中正好碰到了一个需求,需要将旧系统中大量的 HTML 页面嵌入到 Vue 的项目中,嵌入方式是采用 Iframe,但是若使用 Iframe 嵌入页面,那么页面缓存就相当重要,否则用户每次切换到 Iframe 页面类型的菜单,都要重新加载页面,这样非常浪费时间,并且很影响用户的体验。经过网上查阅资料以及结合项目情况,这里记录一下本次问题的解决思路及实现方案。

项目框架:jeecgboot/ant-design-vue-jeecg: 基于 Vue2 + AntDesignVue 实现的 Ant Design Pro(github.com)

参考:vue 应用缓存 iframe 页面 - 掘金 (juejin.cn)

解决思路

  1. 通过自定义规则区分 Iframe 类型路由与非 Iframe 类型路由
  2. 手动获取 Iframe 类型路由的路由信息,并且手动注册所有的 Iframe 路由组件
  3. Iframe 页面打开时或页面切换时,设置对应的 Iframe 组件为打开状态
  4. 获取所有已打开的 Iframe 路由组件,v-for 循环展示 Iframe 路由组件,并根据当前路由信息结合组件信息判断,使用 v-show 控制组件的显隐

关键:将所有 Iframe 类型页面全部注册为 Vue 组件,并且通过打开状态进行展示(懒加载),通过路由信息来控制组件的显隐状态(缓存)

实现方案

区分 Iframe 路由

本项目中是使用路由信息中 meta 数据中的 componentName 来判断的,只要 componentName 包含 Iframe 就代表是 Iframe 类型的路由(区分规则以实际项目为准)

获取所有的 Iframe 路由组件

本项目中采用了动态路由,数据存放在 Vuex 中,用户登录时从数据库加载当前用户的路由菜单。并且项目脚手架是使用 Ant Design Pro,采用的是 Tab 多页签模式,所以在关闭 Tab 时也需要修改 Iframe 组件的状态,所以将 Iframe 组件列表也存放在 Vuex 中,方便在不同组件中共享数据

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
computed: {
...mapState({
// 动态路由
mainMenu: state => state.user.allPermissionList,
iframeComponentList: state => state.app.iframeComponentList
})
},
methods: {
getIframeComponentList() {
const iframeMenuList = this.handleIframe(this.mainMenu, [])
const iframeComponentList = iframeMenuList.map((item, index) => {
// 设置组件
let component = "";
if(item.component.indexOf("layouts")>=0){
component = "components/"+item.component;
}else{
component = "views/"+item.component;
}
const componentPath = resolve => require(['@/' + component+'.vue'], resolve)
// 根据 index 生成唯一的组件名称
const name = `iframe-${index}`
return {
hasOpen: false, // 是否打开过,默认false
...item,
name, // 组件名称
component: componentPath // 组件文件的引用
}
})
// 交给vuex管理
this.$store.dispatch('setIframeComponentList', iframeComponentList)
},
handleIframe(menu, newMenu) {
menu.forEach((item) => {
// 根据规则判断是否为 Iframe 类型的路由
if (item.meta && item.meta.componentName && item.meta.componentName.includes('Iframe')) {
newMenu.push(item)
}
if (item.children && item.children.length) {
this.handleIframe(item.children, newMenu)
}
})
return newMenu
}
}

设置 Iframe 路由在打开后为打开状态,用于缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
computed: {
// 实现懒加载,只渲染已经打开过(hasOpen:true)的 iframe 页
hasOpenComponentList() {
return this.iframeComponentList.filter((item) => item.hasOpen)
}
},
methods: {
// 根据当前路由设置 hasOpen
isOpenIframePage() {
const target = this.iframeComponentList.find((item) => {
return this.$route.path.includes(item.path)
})
if (target && !target.hasOpen) {
let index = this.iframeComponentList.indexOf(target)
let hasOpen = true
// 更改 vuex 中 iframe 路由列表中的打开状态
this.$store.dispatch('setIframeComponentHasOpen', { index, hasOpen })
}
}
}

使用 Iframe 路由组件

1
2
3
4
5
6
7
<component
v-for="item in hasOpenComponentList"
:key="item.name"
:is="item.name"
:linkUrl="item.meta.url"
v-show="$route.path.includes(item.path)"
></component>

Iframe 路由出口组件完整代码

在项目中的菜单页面的路由出口处,将 route-view 组件更换成以下封装的 iframe-route-view 即可使用

注意:这里的 route-viewJEECG 框架在 vue-router 官方提供的 router-view 上封装了一层的自定义组件

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<template>
<div class='main'>
<!-- 非 iframe 页 -->
<route-view v-if='!$route.meta.componentName.includes("Iframe")'></route-view>

<!-- iframe 页 -->
<template v-if='hasOpenComponentList.length'>
<component
v-for='item in hasOpenComponentList'
:key='item.name'
:is='item.name'
:linkUrl='item.meta.url'
v-show='$route.path.includes(item.path)'
></component>
</template>
</div>
</template>

<script>
import Vue from 'vue'
import { mapState } from 'vuex'
import RouteView from '@/components/layouts/RouteView'

export default {
name: 'IframeRouteView',
components: {
RouteView
},
created() {
if (this.iframeComponentList.length === 0) {
this.getIframeComponentList()
}
// 注册 iframe 页组件
this.iframeComponentList.forEach((item) => {
Vue.component(item.name, item.component)
})
// 初始化判断当前路由是否 iframe 页
this.isOpenIframePage()
},
watch: {
$route() {
if (this.iframeComponentList.length === 0) {
this.getIframeComponentList()
}
// 判断当前路由是否iframe页
this.isOpenIframePage()
}
},
computed: {
...mapState({
// 动态主路由
mainMenu: state => state.user.allPermissionList,
iframeComponentList: state => state.app.iframeComponentList
}),
// 实现懒加载,只渲染已经打开过(hasOpen:true)的 iframe 页
hasOpenComponentList() {
return this.iframeComponentList.filter((item) => item.hasOpen)
}
},
methods: {
// 根据当前路由设置hasOpen
isOpenIframePage() {
const target = this.iframeComponentList.find((item) => {
return this.$route.path.includes(item.path)
})
if (target && !target.hasOpen) {
let index = this.iframeComponentList.indexOf(target)
let hasOpen = true
// 更改 vuex 中 iframe 路由列表中的打开状态
this.$store.dispatch('setIframeComponentHasOpen', { index, hasOpen })
}
},
getIframeComponentList() {
const iframeMenuList = this.handleIframe(this.mainMenu, [])
const iframeComponentList = iframeMenuList.map((item, index) => {
// 设置组件
let component = "";
if(item.component.indexOf("layouts")>=0){
component = "components/"+item.component;
}else{
component = "views/"+item.component;
}
const componentPath = resolve => require(['@/' + component+'.vue'], resolve)
// 根据 index 生成唯一的组件名称
const name = `iframe-${index}`
return {
hasOpen: false, // 是否打开过,默认false
...item,
name, // 组件名称
component: componentPath // 组件文件的引用
}
})
// 交给 vuex 管理
this.$store.dispatch('setIframeComponentList', iframeComponentList)
},
handleIframe(menu, newMenu) {
menu.forEach((item) => {
// push所有的iframe节点
if (item.meta && item.meta.componentName && item.meta.componentName.includes('Iframe')) {
newMenu.push(item)
}
if (item.children && item.children.length) {
this.handleIframe(item.children, newMenu)
}
})
return newMenu
}
}
}
</script>
<style lang='less'>
</style>

扩展一:使用 Vuex 管理 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
import Vue from 'vue'

const IFRAME_COMPONENT_LIST = "IFRAME_COMPONENT_LIST"

const iframe = {
state: {
iframeComponentList: [], //iframe组件列表
},
mutations: {
SET_IFRAME_COMPONENT_LIST (state, iframeComponentList) {
Vue.ls.set(IFRAME_COMPONENT_LIST, iframeComponentList)
state.iframeComponentList = iframeComponentList
},
SET_IFRAME_COMPONENT_HAS_OPENED (state, {index, hasOpen}) {
state.iframeComponentList[index].hasOpen = hasOpen
Vue.ls.set(IFRAME_COMPONENT_LIST, state.iframeComponentList)
}
},
actions: {
setIframeComponentList({ commit }, iframeComponentList) {
commit('SET_IFRAME_COMPONENT_LIST', iframeComponentList)
},
setIframeComponentHasOpen({ commit }, {index, hasOpen}) {
commit('SET_IFRAME_COMPONENT_HAS_OPENED', {index, hasOpen})
}
}
}

export default iframe

扩展二:在关闭 Tab 标签页时改变页面的打开状态

注意:以下修改方式仅适用于当前项目,经供参考

1
2
3
4
5
close(pageKey) {
let index = this.iframeComponentList.findIndex(item => pageKey.includes(item.path))
let hasOpen = false
this.$store.dispatch('setIframeComponentHasOpen', { index, hasOpen })
}

总结

整体的实现思路并不复杂,但是运用到实际项目中还是会牵动到项目框架本身的结构,可能会引起其他的问题,个人工作记录,仅供参考,如有不足之处,欢迎友友们在文章末尾进行留言交流指正。