目标
- 页面打开记录以标签形式展示在页面头部,每个标签都会保留上一次的操作缓存。
- 头部标签支持删除,刷新,复制。
- 同一个模块(.vue文件)被多个路由引用时,每个都能保留独立缓存。
最终效果
- 重复打开关闭页面后的内存曲线(chrome的控制台 - More tools/Performance monitor),曲线为锯齿形,说明缓存是正常创建和销毁了。

相关方案
缓存销毁
- vue内部的include和exclude可以实现静态路由(项目运行起来前创建的路由)的创建销毁。
- 如果项目内不存在公用组件(.vue文件被多个路由引用并打开)的问题,缓存的创建和销毁可以通过控制include和exclude来实现。
- 动态路由的话通过更改绑定在vue-router的key值可以刷新掉缓存,但上一个key对应的缓存数据没有自动清除,需要手动清理。
路由配置
<keep-alive>
<router-view v-if="$route.meta.keepAlive" :key="$route.meta.timeKey"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
<keep-alive :include="arrRouterAlive">
<router-view :key="$route.meta.timeKey"/>
</keep-alive>
同一个模块(.vue文件)被多个路由引用并打开多个路由标签时,标签分别能保留独自缓存。
- 在全局的beforeRouter内写逻辑,判断是否进入到了处理路由的组件内,进入的话需要重新创建路由,来保证独立缓存。
- 要考虑用户访问的是新生成的路由链接的情况。
- 不能每次都是新生成路由,因为router只有添加路由的方法,没有删除路由的方法,已经创建的并且已经关闭的需要重复利用。
class routerMethods{
getOnlyKey() {
...
return onlyKey;
}
}
const RouterMethods = new routerMethods()
{
path: 'testEdit',
name: 'editTransfer',
component: editComponents,
meta:{
isTransfer: true,
_name: 'editTransfer',
timeKey: RouterMethods.getOnlyKey()
}
},
router.beforeEach((to, from, next) => {
...
let { name, path, meta } = to;
const isTransfer = meta && meta.isTransfer;
let redirectRouterName = '';
let redirectRouterPath = '';
let onlyKey = RouterMethods.getOnlyKey();
let isAddRouter = false;
if(isTransfer){
isAddRouter = true;
redirectRouterName = name;
redirectRouterPath = `${path}/_redirect_${name}/`;
} else if(path.indexOf('_redirect_') > -1){
let metaName = meta && meta._name;
if(!metaName){
isAddRouter = true;
let arrNameSplit = path.match(/\/_redirect_\S*\//)[0].split('/');
redirectRouterName = arrNameSplit[arrNameSplit.length - 2].split('_redirect_')[1];
redirectRouterPath = `${path.match(/\S*_redirect_\S*\//)[0]}`;
onlyKey = path.match(new RegExp(`_redirect_${redirectRouterName}\/\\S*`))[0].split('/')[1];
}
}
if(isAddRouter){
let closeRouter = router.options.routes.find(a => {
return a.meta && a.meta.closed && a.meta._name === redirectRouterName
})
if(closeRouter){
closeRouter.meta.closed = false;
next({
name: closeRouter.name
})
} else {
next(false)
let itemRouter = router.options.routes.find(a => {
return a.name === redirectRouterName
})
let meta = {
...itemRouter.meta,
timeKey: onlyKey,
_name: redirectRouterName
}
delete meta.isTransfer;
let addRouterName = `_redirect_${redirectRouterName}/${onlyKey}`
let addItem = [{
...itemRouter,
name: addRouterName,
path: `${itemRouter.path}/_redirect_${redirectRouterName}/${onlyKey}`,
component: itemRouter.component,
meta
}]
router.options.routes = router.options.routes.concat(addItem)
router.addRoutes(addItem)
next({
path: `${redirectRouterPath}${onlyKey}`
})
}
} else {
next()
}
...
})
销毁缓存路由
- 通过更改router-view的key值可以刷新组件,但是上一个key的缓存还是会被保留下来,打开很多个页面后就会爆栈,所以需要把历史key对应缓存销毁掉,用到的vue api就是$destroy。
- store内存记录最新的timeKey,然后在缓存组件内监听timeKey数据变化,变化后去比对timeKey,除了最新的timeKey组件保留外,其他的销毁(被缓存的组件生命周期还是会执行)。
data(){
__routeKeepAlive: false,
__routeTimeKey: null,
__routeName: null,
__route_name: null
},
mounted() {
let route = this.$route
if (route) {
this.__routeKeepAlive = !!route.meta.keepAlive;
this.__routeTimeKey = route.meta.timeKey;
this.__routeName = route.name;
this.__route_name = route.meta._name;
}
},
computed: {
__navHisList() {
return this.$store && this.$store.state._navHisList
},
removeCacheName() {
return this.$store.state.removeCacheName;
},
},
methods: {
destroyVueItem() {
if (!this.__routeKeepAlive) return;
if (this.$vnode && this.$vnode.data.keepAlive) {
if (this.$vnode.parent && this.$vnode.parent.componentInstance && this.$vnode.parent.componentInstance.cache) {
if (this.$vnode.componentOptions) {
var key = this.$vnode.key == null ? this.$vnode.componentOptions.Ctor.cid + (this.$vnode.componentOptions.tag ? `::${this.$vnode.componentOptions.tag}` : '') :
this.$vnode.key;
var cache = this.$vnode.parent.componentInstance.cache;
var keys = this.$vnode.parent.componentInstance.keys;
if (cache[key]) {
if (keys.length) {
var index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
}
}
delete cache[key];
}
}
}
}
this.$destroy();
let __routeName = this.__routeName;
this.$store.commit('setCacheNameRemoveResolve', __routeName);
}
},
watch: {
removeCacheName(cacheName) {
if (!this.__routeKeepAlive) return;
let __routeName = this.__routeName;
cacheName && cacheName.hasOwnProperty(__routeName) && this.destroyVueItem();
},
}
刷新标签路由
- 先更改当前路由的meta.timeKey,然后同步该timeKey到store内。
- 已缓存的组件会比对timeKey来判断是否销毁。
refreshPage(item) {
// 更改路由的缓存
let router = this.$router
let r_name = item.name
let targetRouter = null
router.options.routes.some(a => {
let exit = false
if (a.name === r_name) {
targetRouter = a
exit = true
} else if (a.hasOwnProperty('children')) {
a.children.some(b => {
if (b.name === r_name) {
targetRouter = b
exit = true
return true
}
})
}
if (exit) return true
})
if (targetRouter) {
const onlyKey = 随机值
target.meta.timeKey = onlyKey
this.$store.commit('setCacheNameRemovePending', targetRouter.name)
}
},
删除标签路由
- 删除的话就是删除store的arrTagRouters内的当前项。
- 然后更改router.options.routes内的组件meta.timeKey,更改后被缓存的组件会走销毁流程。
- 这里要注意:被删除的组件meta.closed要设置为true,保留路由的重复利用。
clickNaviHisDel(item) {
// 删除store的保存标签的匹配数据
...
// 更改 清除关闭路由的缓存
let router = this.$router
let r_name = item.name
let targetRouter = null
router.options.routes.some(a => {
let exit = false
if (a.name === r_name) {
targetRouter = a
exit = true
} else if (a.hasOwnProperty("children")) {
a.children.some(b => {
if (b.name === r_name) {
targetRouter = b
exit = true
return true
}
})
}
if (exit) return true
})
// 后清除缓存,先清除的话会刷新当前关闭标签
setTimeout(() => {
if (targetRouter) {
targetRouter.meta.closed = true
const onlyKey = 随机值
target.meta.timeKey = onlyKey
this.$store.commit('setCacheNameRemovePending', targetRouter.name)
}
})
}
复制标签路由
- 复制的逻辑相当于手动拼接一个带有_redirect_[name]的路由链接出来。
- 访问这个链接的话就可以直接走beforeRouter的第二个if了,后续会自动生成路由。
copyPath(){
let route = this.$route
let { fullPath, name } = route
// 重定向路由
let newUrl = ''
let onlyKey = 随机值
if (fullPath.indexOf('_redirect_') > -1) {
newUrl = `${fullPath.slice(0, fullPath.lastIndexOf("/"))}/${onlyKey}`
} else {
newUrl = `${fullPath}/_redirect_${name}/${onlyKey}`
}
if (newUrl) location.href = `
}
踩过的几个坑
- keepAlive对于多级路由的兼容性不好,所以不要超过2级或者统一转换成一级。
- 新生成router的components需要指向已存在路由。
- router新增后options.routes不会自动添加,需要手动加进去。
- 偶尔改变timeKey后,router-view不会自动刷新,可以通过在router-view加个v-if来解决。
- 新生成的路由的话没法通过更改includes和excludes来销毁缓存,$destroy可以销毁,但要注意需要把dom内存同样销毁掉,要不会爆栈的。
参考
- vue(v2.6.11)源码,相关的keep-alive源码位置在src/core/components/keep-alive.js
原文 blog.csdn.net/weixin_4107…