vue keep-alive使用,多标签保留缓存,刷新、删除、复制标签,缓存销毁,缓存爆栈解决
目标页面打开记录展示在页面头部,每个标签都会保留上一次的操作缓存头部标签支持删除,刷新,复制同一个页面(表单的编辑)生成多个标签时每个都能保留独立缓存方案保留缓存,使用keep-alive来实现缓存,两种使用方式// 第一种<keep-alive><router-view v-if="$route.meta.keepAlive" :key="$route.meta.timeKey
·
目标
- 页面打开记录展示在页面头部,每个标签都会保留上一次的操作缓存
- 头部标签支持删除,刷新,复制
- 同一个页面(一个.vue生成新的路由)生成多个标签时每个都能保留独立缓存
效果
重复打开关闭页面后的内存曲线(chrome的控制台 - More tools/Performance monitor),曲线为锯齿形,说明缓存是正常创建和销毁了
方案
缓存销毁
- vue内部的include和exclude可以实现静态路由的销毁,静态路由指代码运行起来前创建的路由
- 动态路由的话通过更改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>
// arrRouterAlive为要保留缓存的组件name的集合
// timeKey为实现刷新的标识值
同一个表单页通过不同的配置生成多个路由
/**
* 在路由的beforeRouter内写逻辑
* 判断是否进入到了需要创建路由的组件内,进入的话就重新添加路由
* 要考虑用户访问新生成的路由的链接的情况
* 不能每次都是新生成路由,因为router只有添加路由的方法,没有删除路由的方法,已经创建的并且已经关闭的需要重复利用
* store需要存储要删除的路由removeCacheName: {}
*/
// 几个公用方法
class routerMethods{
// 获取唯一值
getOnlyKey() {
...
return onlyKey;
}
}
const RouterMethods = new routerMethods()
// 路由配置
{
path: 'testEdit',
name: 'editTransfer',
component: editComponents,
meta:{
// true 基础路由,检测到需要生成新的路由
isTransfer: true,
// 用来判断新生成的并且被关闭的路由时使用
_name: 'editTransfer',
// 路由唯一标识置,可以在路由export时统一设置
timeKey: RouterMethods.getOnlyKey()
}
},
// 这里要注意,keepAlive对于多级路由的兼容性不好,所以不要超过2级或者统一转换成一级
// beforeEach加入判断路由生成逻辑
router.beforeEach((to, from, next) => {
...
let { name, path, meta } = to;
// 获取是否需要转发
const isTransfer = meta && meta.isTransfer;
// 要跳转的基础路由配置name
let redirectRouterName = '';
// 要添加路由的path
let redirectRouterPath = '';
// 要生成的路由标识
let onlyKey = RouterMethods.getOnlyKey();
// 是否需要重新添加路由
let isAddRouter = false;
// 检测到name为需要生成路由的name
if(isTransfer){
/**
* 转发路径,需要添加路由到router内
* _redirect_ + 路由名称 做标识,可以在直接访问路径时解析出原始name
*/
isAddRouter = true;
redirectRouterName = name;
redirectRouterPath = `${path}/_redirect_${name}/`;
} else if(path.indexOf('_redirect_') > -1){
// 被转发的新路径
let metaName = meta && meta._name;
// 不存在meta的name时则为通过链接访问的,需要添加路由
if(!metaName){
isAddRouter = true;
// 解析缓存路由name
let arrNameSplit = path.match(/\/_redirect_\S*\//)[0].split('/');
redirectRouterName = arrNameSplit[arrNameSplit.length - 2].split('_redirect_')[1];
redirectRouterPath = `${path.match(/\S*_redirect_\S*\//)[0]}`;
// 解析path的标识值
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
}
// 去除meta的isTransfer属性
delete meta.isTransfer;
let addRouterName = `_redirect_${redirectRouterName}/${onlyKey}`
let addItem = [{
...itemRouter,
name: addRouterName,
path: `${itemRouter.path}/_redirect_${redirectRouterName}/${onlyKey}`,
// 这里要注意,新生成router的components需要指向从已存在路由
component: itemRouter.component,
meta
}]
// 这里要注意,router新增后options.routes不会自动添加,需要手动加进去
router.options.routes = router.options.routes.concat(addItem)
router.addRoutes(addItem)
next({
path: `${redirectRouterPath}${onlyKey}`
})
}
} else {
next(true)
}
...
})
销毁缓存路由
/**
* 通过更改router-view的key值可以刷新组件,但是上一个key的缓存还是会被保留下来,打开很多个页面后就会爆栈,所以需要把历史key对应缓存销毁掉,用到的vue api就是$destroy
* store内存一分最新的timeKey,然后在缓存组件的生命周期内监听timeKey数据变化,变化后去比对timeKey,除了最新的timeKey组件保留外,其他的销毁(被缓存的组件生命周期还是会执行)
*/
// 定义mixin,需要缓存的页面引入
data(){
// 路由的keepAlive
__routeKeepAlive: false,
// 路由的timeKey
__routeTimeKey: null,
// 路由的name
__routeName: null,
// 原始name值
__route_name: null
},
mounted() {
let route = this.$route
if (route) {
// route keepAlive
this.__routeKeepAlive = !!route.meta.keepAlive;
// route timeKey
this.__routeTimeKey = route.meta.timeKey;
// route name
this.__routeName = route.name;
// route _name
this.__route_name = route.meta._name;
}
},
computed: {
// tag项
__navHisList() {
return this.$store && this.$store.state._navHisList
},
// 要删除的路name对象
removeCacheName() {
return this.$store.state.removeCacheName;
},
},
methods: {
销毁缓存方法独立出来
// 处理销毁缓存
destroyVueItem() {
if (!this.__routeKeepAlive) return;
// 逐级强制删除cache,这里就是强制清除缓存值的逻辑
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;
// 去除后的反馈,反馈是要加的,清除路由后把store内存储的待清除路由key清理掉
this.$store.commit('setCacheNameRemoveResolve', __routeName);
}
},
watch: {
// 监听需要删除的name,处理删除操作
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 = `#${newUrl}`;
}
几个坑要注意
- 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
更多推荐
已为社区贡献1条内容
所有评论(0)