记一次KeepAlive组件的爬坑之旅

这一次的项目中需要缓存标签页,Vue-Router的缓存组件KeepAlive,让我重重地摔了一次坑,也让我记录下这段爬坑的心路历程。

首先介绍一下,项目前端架构

1
2
3
4
5
App.vue				--启动页
|-- Home.vue --首页
|-- Abstract.vue --抽象组件,针对二级路由
|-- Form.vue --示例组件
|-- ... --其它组件

我的路由配置文件router.js如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
path: '/'
name: '首页',
component: Home,
children: [{
path: '/example',
name: '示例',
component: Abstract,
children: [{
path: '/form',
name: '表单',
component: Form
}]
}]
}

App.vue的内容如下

1
2
3
<div id="app">
<router-view></router-view>
</div>

Home.vue的内容如下

1
2
3
4
5
6
7
8
9
10
11
<aside>
<el-menu>
...
</el-menu>
</aside>
<section>
<el-tabs>
...
<el-tabs>
<router-view></router-view>
</section>

Abstract.vue的内容如下

1
2
3
<div>
<router-view></router-view>
</div>

以上三个vue组件都用到了vue-router,vue-router的router-view标签,可以根据路由的变化,自动渲染路由对应的组件到router-view标签里。

由于Vue是单页面应用,只要路由发生变动,router-view对应的组件就会重新渲染,那么问题来了,随着路由的更改,如何缓存下每一个路由发生变化前的页面呢?答案是使用keep-alive标签。

Vue-Router的官方文档说只要将router-view嵌在keep-alive标签内,就能实现页面缓存。就像这样

1
2
3
<keep-alive>
<router-view></router-view>
</keep-alive>

我照着葫芦画瓢,给Home.vue文件修改了一下,然而,并没有缓存效果。经过几次尝试后发现,由于我使用了嵌套路由,也就是说Home.vue组件里的router-view标签嵌套了Abstract.vue组件里的router-view标签,所以光给Home.vue文件里加一个keep-alive还不够,还得给Abstract.vue文件里加一个。OK,缓存起效果了。

第二个问题,如何清空标签页缓存,或者叫做如何清空组件缓存。

我使用了Vuex来记录打开过的路由,从而可以实现标签页的功能。在标签页来回切换的时候,缓存的功能看上去很美妙,但是关闭标签页后,需要清空该标签页对应路由组件的缓存,这段用代码该怎么实现呢。

Vue-Router的官方文档资料提到的不多,我是参考了网上给出的建议,给keep-alive加上include特性。如果要实现一个打开标签页a,再关闭标签页a,清空标签页a的缓存的逻辑,代码上需要这么做。

首先是abstract.vue文件,在路由发生变化的时候,往其属性cached数组里添加组件的名称,组件名和二级路由名一致。

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
<keep-alive :include="cached">
<router-view></router-view>
</keep-alive>

export default {
data() {
return {
cached: []
}
},
beforeRouteEnter: function(to, from, next) {
next(vm => {
...
// 允许标签页缓存
var name = to.path.split('/')[2];
var index = vm.cached.indexOf(name);
if(index === -1){
cached.push(name);
}
});
}
watch: {
'$route'(to) {
...
// 允许标签页缓存
var name = to.path.split('/')[2];
var index = this.cached.indexOf(name);
if(index === -1){
this.cached.push(name);
}
}
}
}

然后是Home.vue文件,在删除tab页时,从abstract组件里属性cached数组移除要清空缓存的组件名。

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
<keep-alive>
<router-view></router-view>
</keep-alive>

export default {
name: 'home',
methods: {
tabRemove(target){
...
// 清空标签页缓存
// 找到Abstract组件
var abstract = this.$children[...].$children.find(function(children){
return children.$data.hasOwnProperty('cached');
});
if(abstract){
var cached = abstracct.$data.cached;
var name = target.split('/')[2];
var index = cached.indexOf(name);
if(index > -1){
cached.splice(index, 1);
}
}
}
}
}

以上这段代码,cached是一个数组,根据组件的name属性,动态添加和删除组件。include特性的功能主要有,如果想要缓存的话,只要将组件名添加到cached数组里,如果想要清空组件缓存的话,只要将组件名从cached数组里移除。

问题是我照着网上的方法修改了之后,提示错误信息

include只接受字符串和正则表达式,不接受数组。

我在网上找的一个demo,加上了include特性之后,发现可以清空组件缓存。我就纳闷了,后来经过对比发现,demo用的vue以及vue-router版本要高于我项目中用到的版本,果断修改了package.json文件里的vue以及vue-router版本,执行npm install升级。

终于不再报错,虽然可以清空标签页的缓存了,但是有个bug,最后一个关闭的标签页缓存仍然存在,用户只要关闭标签页后再次点击该标签页对应的菜单,会发现标签页的内容没有刷新,影响用户体验。

最后我想到了一个方法,给Home.vue同样增加一个cached数组,并且在Home.vue文件里的keep-alive标签增加include特性,在新增标签页的时候,往Home组件里的cached数组,动态添加abstract组件的组件名,然后在关闭最后一个标签页的时候,清空cached数组。这样就可以达到完美的效果。

Home.vue文件的修改

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
<keep-alive :include="cached">
<router-view></router-view>
</keep-alive>

export default {
name: 'home',
data(){
return {
cached: []
}
}
methods: {
tabRemove(target){
...
// 清空标签页缓存
// 找到Abstract组件
var abstract = this.$children[...].$children.find(function(children){
return children.$data.hasOwnProperty('cached');
});
if(abstract){
var cached = abstracct.$data.cached;
var name = target.split('/')[2];
var index = cached.indexOf(name);
if(index > -1){
cached.splice(index, 1);
}
if(cached.length === 0) {
//动态删除abstract组件名
this.cached.pop();
}
}
}
}
watch: {
'$route'(to) {
...
//动态添加abstract组件名
if(this.cached.indexOf('abstract') === -1){
this.cached.push('abstract');
}
}
}
}

Abstract.vue文件添加组件名

1
2
3
4
export default {
name: 'abstract'
...
}

满心欢喜的我兴冲冲地以为标签页缓存的功能已经完成了,但是随着发布到生产环境以后发现,某些不该缓存的页面出现了缓存,最后一个标签页关闭,再次打开仍然有缓存。这到底是怎么回事?

一脸懵逼的我,又一次陷入了绝望中。本来开发环境下还能用Vue.js devtools工具来查看Vue生成的结构,但是生产环境不允许,好在网上找到了方案。修改main.js配置文件,加入一行

1
2
...
Vue.config.devtools = true

这样就能够在生产环境下也能查看Vue的结构了。

经过查看发现,原来生产环境下的嵌套路由,即Abstract组件,它只要一级路由发生变动,就会生成一个新的Abstract组件。而开发环境下没有这个问题,无论一级路由怎么变,都只会共用一个Abstract组件。出现这种状况我都不知道该怎么提问了。原来可以通过给Home组件的cached数组属性,动态添加abstract组件名来清空最后一个关闭的标签页的缓存,现在也不能用了。最差的体验就是每一个最后关闭的一级路由下的二级路由缓存都无法清除。难道真的一点办法都没有了吗?

最后我不得不采用的方案就是,去除嵌套路由,不再使用Abstract组件,只用Home组件里的router-view标签。而我所要做的就是对路由配置文件router.js,修改结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
path: '/'
name: '首页',
component: Home,
children: [{
path: '/example/form',
name: '示例',
component: Form,
meta: {
name: '表单'
},
...
}]
}

Home.vue文件的修改如下

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
<keep-alive :include="cached">
<router-view></router-view>
</keep-alive>

export default {
name: 'home',
data() {
return {
cached: []
}
},
methods: {
tabRemove(target){
...
// 清空标签页缓存
var name = target.split('/')[2];
var index = this.cached.indexOf(name);
if(index > -1){
this.cached.splice(index, 1);
}
}
}
watch: {
'$route'(to) {
...
// 允许标签页缓存
var name = to.path.split('/')[2];
var index = this.cached.indexOf(name);
if(index === -1){
this.cached.push(name);
}
}
}
}

没想到这种方法,对清空标签页缓存竟然很友好。我也从没想到使用了嵌套路由后,生产环境的Vue组件结构竟然会和开发环境不一样。当然我想这肯定有其它的解决办法,只是我不知道怎么整理语言来提出这个问题。我选择的是改变路由结构,只用一级路由的办法,绕开了这个不知道怎么解决的问题。

虽然踩过了很多坑,失败了无数次,但是最后完美解决标签页缓存的时候,还是很有成就感的,我想这就是选择作为一名开发的乐趣之一吧。

补充

Keep-Alive缓存标签页后在IE11下发现了一个BUG,如果帐户由一个账户切换另一个账户,上一个账户缓存的页面数据,在切换后的账户中没有得到刷新。目测是IE11在创建页面的时候没有去请求,而是从本地浏览器的缓存里去取了。解决办法是给请求加一个时间戳。即给created或者mount钩子函数里的请求加上一个时间戳。

avatar

chilihotpot

You Are The JavaScript In My HTML