keep-alive解决多层<router-view/>嵌套问题

1,364 阅读1分钟

问题描述

当我们相对页面组件进行缓存时,需要使用<keep-alive>包裹<router-view>

<keep-alive :include="cacheViews">
    <router-view/>
</keep-alive>

如果我们只有一级路由,这样是没毛病的。但是在大多数情况下,一个网站的路由都存在多级嵌套的情况(也就是存在router-view嵌套的情况)。此时由于keep-alive只能作用到下一层<router-view>,导致页面组件缓存不生效

image.png

此时我在网上搜索解决方案,找到了使用操作$route.matched数组来拍平路由嵌套的解决方案:传送门

解决方案

image.png 此前在Menu3组件前还嵌套了两层Layout组件,经过拍平处理后,过滤掉了两层Layout。具体代码如下:

function handleKeepAlive(to) {
  if (to.matched && to.matched.length > 2) {
    for (let i = 0; i < to.matched.length; i++) {
      const element = to.matched[i];
      if (element.components.default.name === 'pageView') {
        to.matched.splice(i, 1);
        handleKeepAlive(to);
      }
    }
  }
}
router.beforeEach((to, from, next) => {
    //....
    handleKeepAlive(to)
    //....
}

遗留问题

这种简单粗暴的方法会不会有问题,在网上又着了一下,果然有问题:传送门

由于暴力的修改了matched会导致面包屑或者用到matched的地方出现问题。解决方案就是备份一份原始的matched给面包屑去用。

matched与router-view的关系

网上大多数都是这种解决方案,但是为什么我们修改了matched就会导致页面路由嵌套的变化呢?我在网上找到了router-view对于matched的使用原理。

router-view是一个 functional 组件,渲染路径匹配到的视图组件。<router-view> 渲染的组件还可以内嵌自己的 <router-view>,根据嵌套路径,渲染嵌套组件

它只有一个名为name的props,这个name还有个默认值,就是default,一般情况下,我们不用传递name,只有在命名视图的情况下,我们需要传递name,命名视图就是在同级展示多个视图,而不是嵌套的展示出来,

router-view组件渲染时是从VueRouter实例._route.matched属性获取需要渲染的组件,也就是我们在vue内部的this.$route.matched上获取的,举个栗子:

  <div id="app">
    <router-link to="/info/">info页</router-link>     
    <router-link to="/info/face">page页</router-link>        
    <hr/>
    <router-view></router-view>
</div>
<script>                   
    const info  = { template:'<div>info Page<router-view><br/></router-view></div>'}             //外层组件
    const page  = { template:'<div>face Page</div>'}                                             //内层组件
    const routes = [                                   
        {
            path:'/info/',
            component:info,
            children:[                         
                {path:'face',component:page}        //使用了嵌套路由
            ]
        }
    ]
    const app = new Vue({                                                     
        el:'#app',
        router:new VueRouter({routes})
    })
</script>

当路由到info页时,控制台打印app.$route.matched

image.png 当路由到page页时,在控制台打印app.$route.matched,输出如下:

image.png

可以看到matched中保存所有父子组件信息,索引从0开始,依次是顶层组件、然后是一层层下来的子组件。router-view组件内部render实现时就会读取这个matched属性的,如下:

var View = {
  name: 'RouterView',
  functional: true,                   //函数式组件
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    var props = ref.props;              //获取props  ;例如:{name: "default"}
    var children = ref.children;        //获取所有子节点
    var parent = ref.parent;            //父组件的引用
    var data = ref.data;

    // used by devtools to display a router-view badge
    data.routerView = true;

    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    var h = parent.$createElement;                                              //获取父组件的$createElement函数引用  这样组件在执行render时可以用命名插槽
    var name = props.name;
    var route = parent.$route;                                                  //当前的路由地址
    var cache = parent._routerViewCache || (parent._routerViewCache = {});      //获取父组件的_routerViewCache属性,如果没有则初始化为空对象

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    var depth = 0;                                      //组件嵌套的层次
    var inactive = false;                               //是否在keep-alive组件内
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      if (parent._inactive) {                             //如果parent._inactive存在
        inactive = true;                                    //则设置inactive为true
      }
      parent = parent.$parent;
    } 
    data.routerViewDepth = depth;                       //组件嵌套的层次

    // render previous view if the tree is inactive and kept-alive
    if (inactive) {
      return h(cache[name], data, children)
    }
 
    var matched = route.matched[depth];                 //从matched属性当中获取当前层次的路由对象,这里保存了需要渲染的组件,这就是上面我们通过app.$route.matched获取的对象
    // render empty node if no matched route
    if (!matched) {
      cache[name] = null;
      return h()
    }

    var component = cache[name] = matched.components[name];     //获取需要渲染的组件

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    data.registerRouteInstance = function (vm, val) {
      // val could be undefined for unregistration
      var current = matched.instances[name];
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val;
      }
    }

    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
      matched.instances[name] = vnode.componentInstance;
    };

    // resolve props
    var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass);
      // pass non-declared props as attrs
      var attrs = data.attrs = data.attrs || {};
      for (var key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key];
          delete propsToPass[key];
        }
      }
    }

    return h(component, data, children)                 //最后渲染该组件
  }
}

所以router-view通过判断当前组件的嵌套层次,然后通过这个层次从route.matches数组中获取当前需要渲染的组件,最后调用全局的$createElement来创建对应的VNode完成渲染的。