复工当天接到了第一个迭代需求:优化。领导说优化代码、优化功能,优化什么都可以,大家自己提需求。真是瞌睡遇到枕头,我正愁最近思考什么问题,这不就来了嘛。
我看着这个经手3人、迭代3年的项目不禁感叹,只要心态够稳,缝缝补补就又是三年。曾经做过的不少功能经过修改调整最后删除,现在这个项目和3年前的第一个迭代大不一样了,当然,雷点也到处都是。
今天主要说说其中一个雷点------多级路由缓存(以下内容针对vue2)。
一、简单的需求
网站包含注册登录页面、多级菜单、面包屑、内容页和详情页等,基础布局如下:

菜单之间跳转不需要页面缓存,从详情返回上一页时需要缓存(即保留父页面的内容,包括筛选条件、页码等所有东西)。比如:
- 首页进入列表页,再返回首页(被缓存);
- 首页进入列表页,再进入详情页,再返回列表页(被缓存),再返回首页(被缓存)、此时列表页被清空.
接到需求后我最初的想法:"这很简单嘛,多么合理的需求啊,动态控制keep-alive组件就行了,需求分分钟解决。"
keep-alive用法可以参考:vuejs.org/guide/built...
只能说,想法很美好,现实很打脸。开发中keep-alive缓存时灵时不灵,总在我认为问题解决了的时候拉闸。
二、问题分析
布局
系统包含了多种布局,有的页面(注册、登录、中间组件等页面)用了空白的模板页(只需要路由跳转);有的页面(登录之后的内容页)用了有内容的布局。如下所示:
空白模板
<template>
<div class="blank_layout">
<router-view></router-view>
</div>
</template>
有内容的模板
<template>
<div id="BasicLayout" class="basic_layout">
<!-- 公共头部 -->
<ComHeader />
<div class="content-box">
<!-- 左侧菜单 -->
<ComMenu />
<!-- 右侧内容 -->
<div class="right_x">
<!-- 面包屑 -->
<ComBreadcrumb />
<!-- 一级菜单和二级菜单点击出现的页面 -->
<div class="main_x">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
</div>
</div>
</div>
</template>
路由
页面上有多级菜单栏,router文件中也存在多级路由,所以页面上的菜单栏和面包屑就可以根据route路径直接渲染。
路由文件中用BlankLayout构建中间组件,用来多级菜单跳转页面。
router.js
const asyncRouterMap = {
path: "/",
component: () => import("@/views/layouts/BasicLayout"),
children: [
{
name: "Home",
path: "",
component: () => import("@/views/Home.vue"),
meta: {
title: "首页",
keepAlive: false // 不需要缓存
}
},
{
name: "About",
path: "about",
component: () => import("@/views/About.vue"),
meta: {
title: "关于",
keepAlive: true // 需要缓存
}
},
{
name: "List",
path: "",
component: () => import("@/views/layouts/BlankLayout"),
children: [
{
name: "List1",
path: "list/1",
component: () => import("@/views/List1.vue"),
meta: {
title: "列表1",
keepAlive: true
}
},
{
name: "List2",
path: "list/2",
component: () => import("@/views/List2.vue"),
meta: {
title: "列表2",
keepAlive: false
}
},
{
name: "List3",
path: "",
component: () => import("@/views/layouts/BlankLayout"),
children: [
{
name: "List3-1",
path: "list/3/1",
component: () => import("@/views/List3-1.vue"),
meta: {
title: "详情3-1",
keepAlive: false
}
}
]
}
]
}
]
}
问题排查
我按照官网规规矩矩的使用keepAlive,但就是不生效。查了各种资料,也看了网上很多失效情况,比如:
- keepAlive内部只能有一个直属组件,等同于template
- keepAlive直属组件中用了v-for
- include、exclude的写法不符合规范
- 组件没有name,或者include、exclude中写的name和组件name没匹配上(是组件自身的name,而不是router文件中的name)
- 不同路由指向了同一个组件(name相同),这里可以看看keep-alive的源代码
keep-alive的实现原理,文件位置:/src/core/components/keep-alive.js
以上情况都排除后,终于发现了问题:
路由嵌套导致存在了多层router-view,进而导致了keep-alive失效。
三、问题解决
方案一:多级路由变一级路由(排除)
router文件中使用单级路由,即所有路由都平铺,这样就只会存在一个router-view。
缺点:
- router由树结构变成了扁平结构,不能一眼看出菜单的层级关系。
- 菜单栏不能直接从route中获取,要自己另外写。
- 面包屑不能直接从route.matched里面获取,要自己一层一层封装。
方案二:增加字段判断父页面是否从详情页面返回,以决定是否需要刷新页面(最终落实)
在store.js中新增以下配置,默认不刷新,即需要缓存
store.js
export default new Vuex.Store({
state: {
// 是否要刷新页面-列表页面
refreshOrderList: false,
},
mutations: {
// 是否要刷新页面-列表页面
setRefreshOrderList(state, payload) {
state.refreshOrderList = payload;
},
},
});
列表页OrderList.vue新增以下配置:
- 在离开页面时进行判断:如果目的路由是详情页,则不需要刷新页面;否则就需要刷新。
- 页面被缓存,触发activated时重置页面,包括筛选条件等。
OrderList.vue
beforeRouteLeave(to, from, next) {
if (to.name == "OrderDetail") {
this.$store.commit("setRefreshOrderList", false);
} else {
this.$store.commit("setRefreshOrderList", true);
}
next();
},
activated() {
// 刷新页面,重置数据
if (this.$store.state.refreshOrderList) {
this.pageSize = 10;
this.toSearch();
}
},
mounted() {
this.setData();
},
也可以把store.js中的refreshOrderList写在router.js的meta中,和keepAlive同级,相对应的,OrderList.vue中修改时就写作:
OrderList.vue
beforeRouteLeave(to, from, next) {
if (to.name == "OrderDetail") {
from.meta.refreshOrderList = false;
} else {
from.meta.refreshOrderList = true;
}
next();
},
......
缺陷: 从父页面跳到需要缓存的子页面时,会触发父页面的mounted
四、新的实现方法
引入keep-alive-router-view插件,该插件内部封装了keep-alive和router-view。
1. 使用方法
全局注册keep-alive-router-view组件,用keep-alive-router-view代替keep-alive组件:
main.js
import KeepAliveRouterView from 'keep-alive-router-view';
Vue.use(KeepAliveRouterView);
BasicLayout.vue
<template>
<div id="BasicLayout" class="basicLayout">
<!-- 公共头部 -->
<ComHeader />
<div class="content-box">
<!-- 左侧菜单 -->
<ComMenu />
<!-- 右侧内容 -->
<div class="rightBox">
<!-- 面包屑 -->
<ComBreadcrumb />
<!-- 一级菜单和二级菜单点击出现的页面 -->
<div class="mainBox">
<keep-alive-router-view :cache="$route.meta.keepAlive" :defaultCache="true" />
</div>
</div>
</div>
</div>
</template>
2. 具体分析
官网上写到(默认情况下,当您操作$router.back
和$router.go
返回页面时,它会使用缓存,而$router.push
和$route.replace
默认情况下不使用缓存。):
It uses the cache when you operate <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e r . b a c k a n d router.back and </math>router.backandrouter.go to return the page by default, and <math xmlns="http://www.w3.org/1998/Math/MathML"> r o u t e r . p u s h a n d router.push and </math>router.pushandrouter.replace do not use the cache by default.
vue-router中的push、replace、go、back、forward方法写法如下:
vue-router.js
VueRouter.prototype.push = function push (location, onComplete, onAbort) {
var this$1$1 = this;
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
this$1$1.history.push(location, resolve, reject);
})
} else {
this.history.push(location, onComplete, onAbort);
}
};
VueRouter.prototype.replace = function replace (location, onComplete, onAbort) {
var this$1$1 = this;
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
this$1$1.history.replace(location, resolve, reject);
})
} else {
this.history.replace(location, onComplete, onAbort);
}
};
VueRouter.prototype.go = function go (n) {
this.history.go(n);
};
VueRouter.prototype.back = function back () {
this.go(-1);
};
VueRouter.prototype.forward = function forward () {
this.go(1);
};
插件源代码中写到:
插件的main.js
wrap(router) {
const { push, go, replace } = router;
router.push = function(...args) {
const location = args[0];
if (checkSetCache(location)) {
setCache(location);
} else {
wrapRouter.setKeepAlive(wrapRouter.getDefaultCached());
}
return push.apply(this, args);
};
router.replace = function(...args) {
const location = args[0];
if (checkSetCache(location)) {
setCache(location);
} else {
wrapRouter.setKeepAlive(wrapRouter.getDefaultCached());
}
return replace.apply(this, args);
};
router.back = function(options = { cache: true }) {
wrapRouter.setKeepAlive(!!options.cache);
return go.apply(this, [-1, { cache: !!options.cache }]);
};
router.forward = function(options = { cache: true }) {
wrapRouter.setKeepAlive(!!options.cache);
return go.apply(this, [1, { cache: !!options.cache }]);
};
router.go = function(num, options = { cache: true }) {
wrapRouter.setKeepAlive(!!options.cache);
return go.apply(this, [num]);
};
}
再对比vue的keep-alive
可以发现,该插件多了三个props:cache(是否缓存)、name(缓存的组件的名称)、defaultCache(是否默认缓存)
插件的main.js
const KeepAliveRouterView = {
name: 'KeepAliveRouterView',
props: {
cache: Boolean,
include: RegExp,
exclude: RegExp,
max: Number,
name: String,
defaultCache: Boolean,
},
}

官网说到,插件的cache属性和$router接口的cache参数决定了页面是否使用缓存。

查看插件源码可以发现,该插件最终渲染出来的结构如下:
main.js
<div class="keep-alive-cache">
<keep-alive :include="include" :exclude="exclude" :max="max">
<router-view v-if="this.cache" ref="cachedPage" :name="name" :key="fullPath">
</router-view>
</keep-alive>
<router-view v-if="!this.cache" ref="cachedPage" :name="name">
</router-view>
</div>
事情发展到这里,我还是有点疑惑,keep-alive在多级路由嵌套时会失效,但keep-alive-router-view插件不会,到底是哪个地方解决的这个问题呢?
源码看得一知半解,实在是惭愧,给自己留个作业,这块疑惑以后补上。也欢迎大佬指导。

写在最后,最近的天气真是糟糕,周末两天都是严重污染,几十个小时只有2小时空气质量为良,还好我一直刷天气预报,逮到了这2小时,赶紧趁机带娃出去遛遛。
