我正在参与创作者训练营第6期,点击了解活动详情

问题描述

前几天笔者遇到这样一个问题,在项目中使用keep-alive缓存路由,路由是以页签那种方式打开的,当页签切换,我们需要路由缓存,当页签关闭,期望keep-alive中缓存的组件要销毁。问题就是页签关闭了,再次打开相同的路由页签时,还是走的缓存,之前的状态还在,关键还只是有些路由有问题。先看下具体的现象吧:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧

上图中共有两个页面,是笔者为了写这篇文章才配置的,其中 A 页面是正常的那种路由,B 面时不正常的那种路由,从上面的图不难看出来,当打开 A 页面,我在右上角输入框输入了11111,然后关闭了路由再打开,那个11111已经消失了,代表这个页面重新创建了,是符合我们预期的;但当打开B页面时,我们在左上角的输入框输入了11111,关闭再打开之后,11111还在,这就是问题所在。

通过vue-devtools可以很明显看到这个问题,就是B页面的缓存还在,如下图所示:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧

分析问题

然后笔者就开始寻找问题,先看下缓存那里是怎么实现的吧,路由出口:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧
从上面可以看出:include的值就是cacheViews中每条路由的name,那路由的name又是怎么来的❓

系统中的菜单都是配置在数据库中根据权限读取的,数据库中配置了相对于views目录的路由路径,然后在登陆后根据权限读取到菜单,再动态生成路由,路由名字为菜单ID:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧
在动态import组件的时候,会把组件的name也赋值为菜单ID:这样一来当打开路由include中就会包含路由的namekeep-alive就会对其进行缓存。

keep-alive只有当组件的名字存在include中才会缓存。

到这呢只是把缓存怎么搞的弄明白了,问题还是没找到? 这时笔者注意到,路由出口那个Vue组件中,计算属性cachedViews有两个组件名是写死的:

const cvs = ['Schema', 'TemplateFactory']

然后再去看下B页面怎么玩的,B页面和A页面逻辑都一样,只是component有差别。

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧
然后笔者就发现问题所在了,所有正常的页面都是一个页面对应一个Vue组件,所有不正常的页面component都是写死的这两个组件,原来这两个组件是公司低码配置出来的页面的公共入口,所以不能像一一对应的那种去更改组件的name,只能在这里写死,是笔者前几天写的。(原来是自己挖的坑)

所以凡是这种页面,include中的name就相当于没有用了,自然而然当include中这条路由的name没了也不会触发keep-alive删除对应组件的缓存(因为Schema, TemplateFactory是一直存在于include中的),就导致了这个问题。那接下来看下怎么解决吧!

解决问题

既然keep-alive不会帮我们清理缓存,那我们可不可以自己清理呢❓

通过调试keep-alive源码,我们发现有个cache属性,上面就是缓存的组件实例,如下图:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧
那是不是意味着,只要我们能获取到keep-alive的组件实例,就可以自己手动清理❓

那就是在beforeRouteLeave这个钩子中去处理,在Schema组件中加上这个钩子:

beforeRouteLeave(to, from, next) {
  debugger
  next()
},

问题又来了,并拿不到这个keep-alive的组件实例,讲道理应该在$parent$parent

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧

原来keep-alive这个组件是一个abstract的组件,所以keep-alive的实例不会出现在组件的父组件链中

<keep-alive>是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

难道就没有办法了吗❓突然笔者看到了个熟悉的字眼:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧

再点开,就有了:

VueRouter路由页签都关闭了,keep-alive缓存还在?来看看我是怎么解决的吧

那接下来就好办了,完整代码如下:

beforeRouteLeave(to, from, next) {
    try {
      const cachedViews = this.$store.state.tagsView.cachedViews
      const findInCache = cachedViews.find(v => v.path === from.path)
      if(!findInCache) { // 说明是关闭了路由,不是切换
        const keepAliveInstance = this.findKeepAlive()
        if(!keepAliveInstance) {
          return next()
        }
        const { cache } = keepAliveInstance
        const cacheKeys = Object.keys(cache)
        const delKey = cacheKeys.find(key => key.includes(from.path))
        delKey && (cache[delKey] = null)
        Reflect.deleteProperty(cache, delKey)
        this.$destroy() // 调用组件的销毁
      }
      next()
    } catch (error) {
      next();
    }
},
methods: {
    //...
    findKeepAlive() {
      const vnodeParent = this.$vnode.parent
      if(vnodeParent.tag.includes('keep-alive')) {
        return vnodeParent.componentInstance
      }
      return null
    }
    //...
}

✅最终也是完美的解决了问题。

✅如果掘友们有更优雅的方案,欢迎在评论区留言噢