Skip to content

vue 官方路由

vue + vue-router 的逻辑结构和数据流

逻辑上:

text
api --> component --> views --> router

这样组织有助于代码结构。

history 和 hash

hash 相对于 history,使用锚点表示链接,所以使用的真实链接更少(甚至有可能整个网站只有主页一个真实链接),不利于 SEO。

路由使用

vue
<RouterLink to="..." />

这个 router-link 接收 to prop,该 vue 属性可以是:

  • 一个字符串;
  • 或者一个对象,对象中有个 name,推荐使用这种方式,它可以让导航栏的链接和路由进行解耦。

a 标签的样式

router-link 默认有两个样式类 router-link-activerouter-link-exact-active

这两个样式类的区别在于两个是否精确匹配。

比如有两个路由标签分别指向 //home。当我们点击指向 /home 标签的时候,页面的 document.URL 变为了xxx.com/home。此时指向 / 的路由标签的样式变为只有router-link-active,而指向 /home 标签的样式类变为了 router-link-active router-link-exact-active

这两个样式类的名字可以在路由的配置 index.js 中更改,也可以单独为一个 router-link 标签传入同名的 vue 标签属性 来修改。一般不用修改,因为一般样式起的名字不会和这两个样式类的名字冲突。

路由 meta

路由配置 meta 属性可以保存一些全局的信息,这些数据会被子组件 rouer-view 内部的默认插槽作为子组件数据 route 传递出去供我们使用。

html
// <RouterView> 的实现
<templete>
    <Component :is="view">
    <slot :route="meta" :Component="view" ... ></slot>
    </Component>
</templete>

// 实际的使用
<RouterView v-slot="{ route, Component }">
    ...
    <Component :is="Component" />
    ...
<RouterView />

useRoute()

组件内的 useRoute() 返回当前的路由地址相关信息。相当于在模板中使用 $route

js
import { useRoute } from 'vue-router'

const route = useRoute();

得到的 route,含有路由地址的信息。我们知道,路由地址包含了 host, search, hash 等字段。而 route 则提供了这些字段的相关信息:

  • params,表示匹配路由过程中 host 字段的动态参数。
  • query,表示 search 字段处理后的键值对。
  • hash,表示 hash 字段。

动态参数使用时组件不刷新的问题

url 中动态参数的变化不会导致页面中组件的刷新。

比如,/articles/1articles/2 虽然使用点击链接切换,导致了 url 的改变,但这并不会触发 router-view 响应,因为手写过 router-view 就知道,它最终渲染的组件是通过 计算属性监听 url 的变化得到的,就算 url 发生变化,最后的计算属性没有发生变化,即最终渲染的组件是同一个: 都是 /articales/:id 匹配的那个组件。所以 vue 不会重新卸载该组件、再挂载相同的组件。

router-view 被设计的足够简单,以至于它仅仅做的就是根据 url 的变化判断渲染出哪个组件。但通过上面的动态参数的例子知道, url 的变化并不一定导致组件的变化,这种关系并不是充分必要的。解决方法是,在自己用动态参数的时候,自己在组件中监听 url 的变化,然后作出响应刷新本组件中渲染的数据:

js
import { useRoute } from 'vue-router'
const route = useRoute(); // 拿到 url 
const id = route.params.id; // 拿到 id
const article = await api.get(id); // 根据 id 请求对应的文章数据

watch(route, () => { // 监听路由 url 的变化,url一旦改变,就触发自定义钩子
    article = await api.get(route.params.id); // 钩子内部重新发送请求,更新数据, ref 被更新,触发相关 DOM被更新
})

这可以被认为是一个设计缺陷,但我觉得他只是想要设计的足够简单。

因为如果要实现 url 的变化一定会导致目标组件重新渲染,想了一下其实很简单,只需要在 router-view 实现中监听 url 的部分,一旦 url 变化就重新挂载和卸载目标组件就好了,这一步也比较简单,但总归多了一步,可能设计者不想多这一步。

route 对象是一个响应式对象,所以它的任何属性都可以被监听,但你应该避免监听整个 route 对象。在大多数情况下,你应该直接监听你期望改变的参数。

关于 Suspense 和路由结合的一些思考

路由的变化往往伴随着新数据的请求——这将导致异步操作。我一开始习惯于使用 Suspense 包裹一个含有异步操作的组件,但是后来发现这很容易出 bug,并且由于现在来看这个组件仍然在"实验性阶段",所以 Suspense 在 vue 文档上的描述十分的粗略,没有太多的细节可供参考,令人头大。

我的建议是,尽量规避使用 Suspense。它本来就是为了协调多个异步组件而设计的,而这种情况发生的少之又少。对于单个异步组件,我们可以使用 v-if + 骨架屏的方式替代它,成本并不高,可控性更好。

命名视图

router-view 组件有一个 prop 叫做 name ,它默认是 defaultrouter-view 选择哪个组件,实际上是基于 url + name 的。每一个 router-view,根据配置文件 index.js 里的配置信息,找到对应的 url 引入了 哪些组件(Components),然后从这些组件中根据 name 字段找到最终的组件。如果对应 url 的配置项找不到相同的 name,就等于找不到组件,则最终 Component 为空,则不会报错(猜测router-view 里面判断了 Component 为空的情况)。

如果插入一个 <Component :is="Component" />,为空也会抛出警告。

路由嵌套

在命名视图中,知道了 router-view,根据配置文件 index.js 里的配置信息,找到对应的 url。这个寻找的过程是从外向内找的,一旦找到就完事。每个路由 url 有一个 children 属性表示子 url:

js
routes: [
    {
      path: '/home',
      component: HomeView,
      // 子 url
      children: [
        {
          path: '/child', // '/home/child'
          component: ChildView,
        },
      ]
    },
]

url 匹配的过程从外向内,匹配到第一个符合的,比如 /home/child/1router-view,匹配到了 /home,就结束了,不会继续往下匹配。

基于这个规则,还提供了嵌套的 router-view

比如说一个 router-view 匹配到了一个 url,并且该 url 对应一个组件,于是该 router-view 渲染这个组件。渲染过程中,它发现这个组件里又有一个 router-view,于是重复上述过程:这个新的 router-view 依然会经历在 index.js 提供的 url 配置,从外向里匹配的过程。但是不同的是,它不会再从根开始匹配,而是从外层 router-view 的终点,继续向里面进行匹配,得到组件。

参考实现

为什么内层的 router-view 能做到不会再从根开始匹配,而是从外层 router-view 的终点,继续向里面进行匹配?参考了一篇掘金文章:

vue-router参考实现

每个 router-view 在渲染的时候其实都会标记自身。当渲染的组件里又有 router-view,那么这个嵌套的 router-view 就会进行一个简单的递归父级引用的操作,每次递归查看引用是否持有标记,如果有就 count++。一直递归到终点,得到最终的 count,这个 count 就是嵌套的深度(depth)。有了这个深度,就知道应该从外到里匹配成功多少次了。

现在总的来看,整个 router-view 在代码中的嵌套层次像是 多颗互不关联的树。每颗树的每个嵌套层次都按照上面的方式找到自己的目标组件。

路由组件传 prop

第一种方式

vue
<RouterView v-slot="{ Component }">
  <component
    :is="Component"
    view-prop="value"
   />
</RouterView>

通过这种方式,所有的 router-view 都将得到相同的 prop

第二种方式

在路由配置文件中单独为某一个 url 配置想要传递的 prop。这种情况下是基于 url 的,而不是基于 router-view 的。

js
const routes = [
    {
    path: '/user/:id', 
    component: User, 
    props: true
    }
]

多出的 props 的值的类型可以是 boolean, function, object

  • boolean 方式

当类型为 boolean 的时候,props 设置为 true ,route.params 将被设置为组件的 props。

  • function 方式

如果是一个函数,路由将尝试调用该函数并传入 route,也就是当前的路由信息,将函数返回的结果作为 props。这主要用于根据当前的路由信息设置想要传递的 vue 属性。

js
const routes = [
  {
    path: '/search',
    component: SearchUser,
    props: (route) => ({ query: route.query.q })
  }
]
  • props 也可以是一个对象

当 props 是一个对象时,它将原样设置为组件 props。当 props 是静态的时候有用。

props 为对象还发生在命名视图,对于有命名视图的路由,如果想要为某个组件传递 props,必须为该视图中的每个组件定义 props 配置:

js
const routes = [
  {
    path: '/user/:id',
    // 使用components,命名视图
    components: { default: User, sidebar: Sidebar },
    // 内层的行为和上面的规则一样, prop 可以是 boolean、function、object
    props: { default: true, sidebar: false }
  }
]

别名

别名相当于为 url 起了一个别名,仅此而已。别名参与路由匹配,但对于一个给定的 url 来说,只有在正常情况下所有的 url 路由都无法匹配的时候,才会去尝试匹配别名。

重定向

每个路由可以配置一个 redirect,当匹配到的最后, url 发现需要重定向的时候就会重新跳转。