场景

在 vue-router 的使用中,有时会面临这样一个问题,那就是 vue-router 的设计要求 component 层级和 route 层级保持一致。

当有如下的路由定义时:

const route = {
  path: '/list',
  name: 'list',
  component: List,
  children: [{
    path: 'detail/:id',
    name: 'detail',
    component: Detial,
  }]
}

List 组件需在自己的模板内包含 router-view 组件,/listdetail/:id 路由的层级关系要在组件中体现出来。

// in List.vue
<template>
  <div id="list">
    <h5>This is list.</h5>
    <router-view></router-view>
  </div>
</template>

在这种情况下,当导航至 /list/detail/1 时, ListDetail 组件会同时渲染至页面中。如下:

在大多数情况下我们普遍的期望是当进入 detail 路由时,页面中只显示相应的 Detail 组件,而不在显示 List 组件。

问题

这个问题初看很简单,如果只是为了达到 ListDetail 同时只有一个存在,只需将路由重新设计为扁平化的即可。然而这会破坏这两个路由本身所拥有的父子语义,因为 UI 的表现而去破坏语义是不理想的。其次,我们项目中还有一个根据路由层级自动生成的面包屑导航,破坏 listdetail 的父子关系会导致面包屑导航无法正确工作。

思路

    +---------------------+
    |   当前最深匹配的路由   | 
    |       是否与         | 
    |    自身路由匹配       |
    +---------------------+
               |
         yes   |     no
      +--------+--------*
      |                 |
      v                 v  
  +------+        +------------+
  | show |        |    show    |
  | self |        | child route|
  +------+        +------------+    

$route.matched 存放着当前所有匹配的路由, 中的最后一个项就是当前匹配层次最深的路由。我们可以通过路有对象的 instances.default 取得与此路由相关联的 component 实例,然后与当前组件实例比较一下即可。最后我们通过手写 vuerender 方法来完成条件渲染。

最终,我们采取了高阶组件的实现方式,为组件提供子路由匹配时,将自己”隐藏“的功能。

实现:

export default function routeReplaceSelf(component) {
  return {
    name: 'routerReplaceSelf',
    computed: {
      showChild() {
        const deepestMatchedRoute = this.$route.matched[this.$route.matched.length - 1];
        return deepestMatchedRoute.instances.default !== this;
      },
    },
    render(h) {
      return this.showChild ? h('router-view') : h(component);
    },
  };
}

示例:

还有一个小问题,当从子路由返回父组件时,父组件会重新 mount。这里可以借助 keep-alive 来缓存组件避免不必要的 mount

render(h) {
  const child = this.showChild ? h('router-view') : h(component);
  return h('keep-alive', [child]);
},

思考

在解决上述问题时,尤大在相关 issue 内说到

I think this breaks the relationship between route config nesting and router-view nesting and can make things a bit harder to reason about.

对此我是不认可的,强制路由层级和组件层级匹配,这样做增强了二者的耦合性。对路由本身而言它是没有组件这个感念的,毕竟它的名字就叫路由,而不是组件路由,页面路由。路由本身只应作为一个工具,告诉我们当前的 url 是否和其相定义相匹配,之后具体是路由到一个页面,一个组件,还是一个简单的回调则应由用户去控制。react-router (v4) 就是一个充分体现了路由灵活性的实例,借助其我们可以完成很多复杂的需求。这里推荐一下 page stack 这篇文章。