likes
comments
collection
share

解决vue中keep-alive和router-view搭配使用时(多级路由)缓存失效问题复工当天接到了第一个迭代需求:

作者站长头像
站长
· 阅读数 18

复工当天接到了第一个迭代需求:优化。领导说优化代码、优化功能,优化什么都可以,大家自己提需求。真是瞌睡遇到枕头,我正愁最近思考什么问题,这不就来了嘛。

我看着这个经手3人、迭代3年的项目不禁感叹,只要心态够稳,缝缝补补就又是三年。曾经做过的不少功能经过修改调整最后删除,现在这个项目和3年前的第一个迭代大不一样了,当然,雷点也到处都是。

今天主要说说其中一个雷点——多级路由缓存(以下内容针对vue2)。

一、简单的需求

网站包含注册登录页面、多级菜单、面包屑、内容页和详情页等,基础布局如下:

解决vue中keep-alive和router-view搭配使用时(多级路由)缓存失效问题复工当天接到了第一个迭代需求:

菜单之间跳转不需要页面缓存,从详情返回上一页时需要缓存(即保留父页面的内容,包括筛选条件、页码等所有东西)。比如:

  1. 首页进入列表页,再返回首页(被缓存);
  2. 首页进入列表页,再进入详情页,再返回列表页(被缓存),再返回首页(被缓存)、此时列表页被清空.

接到需求后我最初的想法:“这很简单嘛,多么合理的需求啊,动态控制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构建中间组件,用来多级菜单跳转页面。

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,但就是不生效。查了各种资料,也看了网上很多失效情况,比如:

  1. keepAlive内部只能有一个直属组件,等同于template
  2. keepAlive直属组件中用了v-for
  3. include、exclude的写法不符合规范
  4. 组件没有name,或者include、exclude中写的name和组件name没匹配上(是组件自身的name,而不是router文件中的name)
  5. 不同路由指向了同一个组件(name相同),这里可以看看keep-alive的源代码

keep-alive的实现原理,文件位置:/src/core/components/keep-alive.js

以上情况都排除后,终于发现了问题: 路由嵌套导致存在了多层router-view,进而导致了keep-alive失效。

三、问题解决

方案一:多级路由变一级路由(排除)

router文件中使用单级路由,即所有路由都平铺,这样就只会存在一个router-view。

缺点:

  1. router由树结构变成了扁平结构,不能一眼看出菜单的层级关系。
  2. 菜单栏不能直接从route中获取,要自己另外写。
  3. 面包屑不能直接从route.matched里面获取,要自己一层一层封装。

方案二:增加字段判断父页面是否从详情页面返回,以决定是否需要刷新页面(最终落实)

在store.js中新增以下配置,默认不刷新,即需要缓存

export default new Vuex.Store({
  state: {
    // 是否要刷新页面-列表页面
    refreshOrderList: false,
  },
  mutations: {
    // 是否要刷新页面-列表页面
    setRefreshOrderList(state, payload) {
      state.refreshOrderList = payload;
    },
  },
});

列表页OrderList.vue新增以下配置:

  1. 在离开页面时进行判断:如果目的路由是详情页,则不需要刷新页面;否则就需要刷新。
  2. 页面被缓存,触发activated时重置页面,包括筛选条件等。
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中修改时就写作:

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组件:

import KeepAliveRouterView from 'keep-alive-router-view';
Vue.use(KeepAliveRouterView);
<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. keep-alive-router-view的介绍

官网上写到(默认情况下,当您操作$router.back$router.go返回页面时,它会使用缓存,而$router.push$route.replace默认情况下不使用缓存。):

It uses the cache when you operate router.backandrouter.back and router.backandrouter.go to return the page by default, and router.pushandrouter.push and router.pushandrouter.replace do not use the cache by default.

vue-router中的push、replace、go、back、forward方法写法如下:

  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);
  };

插件源代码中写到:

  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(是否默认缓存)

const KeepAliveRouterView = {
  name: 'KeepAliveRouterView',
  props: {
    cache: Boolean,
    include: RegExp,
    exclude: RegExp,
    max: Number,
    name: String,
    defaultCache: Boolean,
  },
}

解决vue中keep-alive和router-view搭配使用时(多级路由)缓存失效问题复工当天接到了第一个迭代需求:

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

解决vue中keep-alive和router-view搭配使用时(多级路由)缓存失效问题复工当天接到了第一个迭代需求:

查看插件源码可以发现,该插件最终渲染出来的结构如下:

<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>

3. keep-alive在多级路由嵌套时为什么会失效?

keep-alive

  1. 在create钩子中,创建一个cache对象,用来保持vnode节点,是key/value形式的,key是组件的key,value是组件的vnode
  2. 在mounted钩子中,监听include和exclude的变动,任一有变动就调用pruneCache方法及时修改缓存中的数据。这里用了缓存淘汰策略LRU方法,以后有机会我们再详细讲讲
  3. 在destroyed钩子中,遍历cache对象的属性键,调用pruneCacheEntry方法清除cache缓存中的所有组件实例
  4. 在render阶段:
    1. 先获取默认插槽及其第一个子组件节点;
    2. 获取该组件节点的name(name不存在就获取tag);
    3. 根据include和exclude条件判断是否需要缓存?include中存在则要缓存,exclude中存在则不缓存,include优先级更高,如果不需要缓存则直接返回vnode,如果需要缓存则继续往下;
    4. 如果命中缓存,则直接从缓存中拿vnode的组件实例,这里注意要调整组件key的顺序:将其从原来的地方删除并重新放在最后一个位置(这里用到了缓存淘汰策略LRU),最后设置keepAlive标记位为true;
    5. 如果没命中,则表示要新增缓存。一方面,要把它添加到栈顶;另一方面,要判断是否超过max缓存最大值,如果超过就要删除最末尾的缓存。

缓存淘汰策略LRU(least recently used 最近最少使用): 根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”

①将新数据从尾部插入到this.keys中 ②每当缓存命中(即缓存数据被访问),则将数据移到this.keys的尾部 ③当this.keys满的时候,将头部的数据丢弃

router-view

再看router-view的工作原理:根据routerView向上遍历父节点,判断当前组件的嵌套层级,根据depth和matched获取要渲染的组件。

var depth = 0; // 组件嵌套的层次
var inactive = false; // 是否在keep-alive组件内

// 判断当前view组件所在的层级:
// 向上遍历父节点 遇到routerView组件则路由深度加1 直到找到顶级节点
while (parent && parent._routerRoot !== parent) {
  var vnodeData = parent.$vnode ? parent.$vnode.data : {};
  if (vnodeData.routerView) {
    depth++;
  }
  // _inactive和_directInactive是判断激活状态的属性(见vue.js)
  if (vnodeData.keepAlive && parent._directInactive && parent._inactive)   {
    inactive = true;
  }
  parent = parent.$parent;
}
data.routerViewDepth = depth;
      
var matched = route.matched[depth];
var component = matched && matched.components[name];

// render empty node if no matched route or no config component
// 如果没有匹配的路由或者配置组件 则渲染空节点
if (!matched || !component) {
  cache[name] = null;
  return h()
}

// cache component
// 否则查找匹配到的路由中的组件 获取要渲染的组件
cache[name] = { component: component };

总结

我在查找资料时看到来自www.jb51.net/article/242… 的说法:

keep-alive的工作原理:实际上是根据组件的名称决定缓存,如果即将缓存的组件名称命中缓存,就直接取缓存里的组件进行渲染。这里的命中缓存是带有父子组件关联关系的。我们对一个页面的渲染,是按一个一个组件来的,组件命中缓存则取缓存,如果没有命中,那么下面的子组件也不再进行缓存命中的判断,而是重新渲染。

还有以下疑惑等待解决:

  1. 对于“这里的命中缓存是带有父子组件关联关系的”我还是不太理解,不知道在源码哪个地方有写。
  2. keep-alive在多级路由嵌套时会失效,但keep-alive-router-view插件不会,代码中到底是哪个地方有解决这个问题呢?

代码看得一知半解,实在是惭愧,给自己留个作业,这块疑惑以后补上。也欢迎大佬指导。

解决vue中keep-alive和router-view搭配使用时(多级路由)缓存失效问题复工当天接到了第一个迭代需求:

题外话

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

解决vue中keep-alive和router-view搭配使用时(多级路由)缓存失效问题复工当天接到了第一个迭代需求:

转载自:https://juejin.cn/post/7310156414252400640
评论
请登录