vue keep-alive使用:多标签保留缓存和销毁缓存;实现标签的刷新、删除、复制;解决缓存导致的浏览器爆栈

1,949 阅读4分钟

目标

  • 页面打开记录以标签形式展示在页面头部,每个标签都会保留上一次的操作缓存。
  • 头部标签支持删除,刷新,复制。
  • 同一个模块(.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>
// arrRouterAlive为要保留缓存的组件(.vue文件)name的集合
// timeKey为实现刷新的标识值
同一个模块(.vue文件)被多个路由引用并打开多个路由标签时,标签分别能保留独自缓存。
  • 在全局的beforeRouter内写逻辑,判断是否进入到了处理路由的组件内,进入的话需要重新创建路由,来保证独立缓存。
  • 要考虑用户访问的是新生成的路由链接的情况。
  • 不能每次都是新生成路由,因为router只有添加路由的方法,没有删除路由的方法,已经创建的并且已经关闭的需要重复利用。
/**
* removeCacheName: store需要存储要删除的路由对象
*/

// 几个公用方法
class routerMethods{
    // 获取唯一值
    getOnlyKey() {
      ...
      return onlyKey;
    }
}
const RouterMethods = new routerMethods()

// 路由配置
{
    path: 'testEdit',
    name: 'editTransfer',
    component: editComponents,
    meta:{
    	// 是否需要生成新的路由的标识
        isTransfer: true,
        // 原始name记录值,用来重复利用时判断name
        _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()
    }
    ...
})
销毁缓存路由
  • 通过更改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

原文 blog.csdn.net/weixin_4107…