剖析 Vue Router 源码中 `RouterView` 和 `RouterLink` 组件的实现,它们如何与路由实例交互。

各位靓仔靓女们,晚上好!我是你们的老朋友,人称“代码界的段子手”的…咳咳,今天咱们不讲段子,讲点硬核的,聊聊 Vue Router 里面的两个重要角色:RouterViewRouterLink

准备好了吗?要发车了!

第一部分:路由的基石:RouterView

RouterView,顾名思义,就是用来“看”路由的组件。它负责根据当前路由,渲染对应的组件。你可以把它想象成一个占位符,一个容器,或者更形象一点,一个“舞台”,路由对应的组件就是在这个舞台上表演的演员。

1. 核心职责:渲染组件

RouterView 的核心职责就是渲染与当前路由匹配的组件。这个“匹配”的过程,是由 Vue Router 的路由匹配算法决定的。一旦匹配成功,RouterView 就会拿到对应的组件,然后把它渲染到页面上。

2. 源码剖析:简单粗暴的渲染

让我们简单看看 RouterView 的源码(简化版,只保留核心逻辑):

// src/components/view.js (简化版)

import { h, inject, computed } from 'vue'
import { RouterViewContext } from '../injectionSymbols'

export const RouterView = {
  name: 'RouterView',
  props: {
    name: {
      type: String,
      default: 'default',
    },
  },
  setup(props, { attrs }) {
    const injectedRoute = inject(RouterViewContext, {}) // 从父级注入路由信息
    const depth = injectedRoute.depth || 0
    const matchedRouteRef = computed(() => {
      const route = injectedRoute.route
      return route.value.matched[depth]
    })

    return () => {
      const route = injectedRoute.route.value;
      const matchedRoute = matchedRouteRef.value;

      if (!matchedRoute) {
        return null // 没有匹配的路由,就什么也不渲染
      }

      const Component = matchedRoute.components[props.name]; // 获取组件
      if (!Component) {
        return null;
      }
      // 渲染组件
      return h(Component, {
          ...attrs,
          route: route
      })
    }
  },
}

这段代码虽然简化了,但足以说明 RouterView 的工作原理:

  • inject(RouterViewContext, {}): 通过 inject API,从父级组件(通常是 Router 组件或者其他的 RouterView)注入路由信息。RouterViewContext 是一个 Symbol,作为依赖注入的 key。这是一种父子组件间传递数据的巧妙方式,避免了 props 逐层传递的麻烦。depth 表示当前 RouterView 的嵌套深度。

  • matchedRouteRef = computed(() => ...): 使用 computed 来创建一个响应式的计算属性,根据当前路由的 matched 数组和 depth,找到匹配的路由记录 (matchedRoute)。matched 数组包含了所有匹配的路由记录,从最外层到最内层。

  • const Component = matchedRoute.components[props.name]: 从 matchedRoutecomponents 属性中,根据 props.name 获取要渲染的组件。props.name 允许我们使用命名视图,同一个路由可以渲染多个组件到不同的 RouterView 中。如果没有指定 name,默认使用 ‘default’。

  • return h(Component, { ...attrs, route: route }): 使用 h 函数(Vue 3 中的 createElement),创建一个 VNode,表示要渲染的组件。同时,将 attrs (传递给 RouterView 的所有 attribute) 和当前 route 对象作为 props 传递给组件。

3. 多层嵌套:递归渲染的艺术

RouterView 支持多层嵌套,这使得我们可以构建复杂的页面结构。当一个路由匹配到多个组件时,每个 RouterView 都会负责渲染其中的一个组件。

例如,我们有以下路由配置:

const routes = [
  {
    path: '/users/:id',
    component: () => import('./views/UserLayout.vue'),
    children: [
      {
        path: 'profile',
        component: () => import('./views/UserProfile.vue'),
      },
      {
        path: 'posts',
        component: () => import('./views/UserPosts.vue'),
      },
    ],
  },
]

UserLayout.vue 中,我们需要放置一个 RouterView 来渲染 UserProfile.vue 或者 UserPosts.vue

<template>
  <div>
    <h1>User Layout</h1>
    <router-view />
  </div>
</template>

当访问 /users/123/profile 时,UserLayout.vue 会被渲染,并且内部的 RouterView 会渲染 UserProfile.vue

4. 命名视图:灵活的布局

命名视图允许我们在同一个路由下,渲染多个组件到不同的 RouterView 中。这对于创建复杂的布局非常有用,比如页面头部、侧边栏、内容区域等等。

路由配置:

const routes = [
  {
    path: '/dashboard',
    components: {
      default: () => import('./views/Dashboard.vue'),
      sidebar: () => import('./components/Sidebar.vue'),
      header: () => import('./components/Header.vue'),
    },
  },
]

模板:

<template>
  <div class="dashboard">
    <header>
      <router-view name="header" />
    </header>
    <aside>
      <router-view name="sidebar" />
    </aside>
    <main>
      <router-view />
    </main>
  </div>
</template>

在这个例子中,/dashboard 路由会同时渲染 Dashboard.vue 到默认的 RouterViewSidebar.vuename 为 "sidebar" 的 RouterViewHeader.vuename 为 "header" 的 RouterView

总结:RouterView 是路由渲染的核心,它通过递归和命名视图,实现了灵活的页面布局。

特性 描述
核心职责 渲染与当前路由匹配的组件
嵌套渲染 支持多层嵌套,实现复杂的页面结构
命名视图 允许在同一个路由下渲染多个组件到不同的 RouterView 中,实现灵活的布局
依赖注入 通过 inject 从父级组件获取路由信息,避免了 props 逐层传递的麻烦

第二部分:路由的导航:RouterLink

RouterLink,是 Vue Router 提供的用于导航的组件。它就像 HTML 中的 <a> 标签,但是它利用 Vue Router 的路由机制,实现了无刷新的页面跳转。

1. 核心职责:创建导航链接

RouterLink 的核心职责是创建一个可以触发路由变化的链接。当用户点击 RouterLink 时,它会调用 Vue Router 的 push 或者 replace 方法,更新浏览器的 URL,并触发 RouterView 的重新渲染。

2. 源码剖析:精心设计的导航

让我们看看 RouterLink 的源码(同样是简化版):

// src/components/link.js (简化版)

import { h, computed, getCurrentInstance, inject } from 'vue'
import { useRouter } from '../composables/router'
import { useRoute } from '../composables/route'

export const RouterLink = {
  name: 'RouterLink',
  props: {
    to: {
      type: [String, Object],
      required: true,
    },
    tag: {
      type: String,
      default: 'a',
    },
    custom: {
      type: Boolean,
    },
    replace: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, { slots }) {
    const router = useRouter() // 获取 router 实例
    const route = useRoute() // 获取 route 实例
    const currentRoute = computed(() => {
      return router.resolve(props.to) // 解析目标路由
    })

    const navigate = (e) => {
      if (isLinkEvent(e)) {
        if (props.replace) {
          router.replace(props.to)
        } else {
          router.push(props.to)
        }
      }
    }

    return () => {
      const { resolved, href } = currentRoute.value;
      const activeClass = 'router-link-active'
      const exactActiveClass = 'router-link-exact-active'
      const currentClasses = [];

      if (resolved.matched.length && resolved.resolved.path === route.path) {
          currentClasses.push(exactActiveClass);
      }

      const link = h(
        props.tag,
        {
          href: href,
          onClick: navigate,
          class: currentClasses
        },
        slots.default && slots.default()
      )
      return props.custom
        ? slots.default({
            href,
            navigate,
            route: resolved
          })
        : link
    }
  },
}

function isLinkEvent(event) {
    return (
      event.button === 0 &&
      isModifierAllowed(event) &&
      (!event.defaultPrevented)
    )
  }

function isModifierAllowed (event) {
    const { metaKey, altKey, ctrlKey, shiftKey } = event
    return !(metaKey || altKey || ctrlKey || shiftKey)
  }

这段代码展示了 RouterLink 的核心逻辑:

  • useRouter()useRoute(): 使用 useRouteruseRoute composables 获取 router 实例和 route 实例。router 实例提供了 pushreplace 等方法,用于导航;route 实例包含了当前路由的信息。

  • currentRoute = computed(() => router.resolve(props.to)): 使用 router.resolve(props.to) 解析 props.to,得到一个包含 resolved (解析后的路由对象) 和 href (实际的 URL) 的对象。router.resolve 非常重要,它负责将 to 属性(可以是字符串或对象)转换为一个完整的路由对象。

  • navigate = (e) => ...: 定义一个 navigate 函数,用于处理点击事件。当用户点击 RouterLink 时,会调用这个函数。它会判断是否是有效的链接点击事件(例如,不是右键点击或者使用了 modifier keys),然后根据 props.replace 的值,调用 router.push 或者 router.replace 方法,进行导航。

  • h(props.tag, { ... }, slots.default && slots.default()): 使用 h 函数创建一个 VNode,表示一个 HTML 元素。props.tag 允许我们指定要渲染的 HTML 标签,默认为 <a>href 属性设置为解析后的 URL,onClick 事件设置为 navigate 函数。slots.default() 渲染 RouterLink 的子节点,也就是链接的文本。

  • activeClassexactActiveClass: 动态添加 CSS 类名,标识当前激活的链接。activeClass 在当前路由是目标路由的子路由时生效,exactActiveClass 在当前路由与目标路由完全匹配时生效。

  • props.custom: 支持自定义渲染。如果 props.customtrue,则 RouterLink 不会直接渲染一个 <a> 标签,而是将 hrefnavigate 函数作为 props 传递给插槽,由用户自定义渲染。

3. to 属性:灵活的路由目标

to 属性是 RouterLink 最重要的属性,它指定了导航的目标路由。to 属性可以是字符串或者对象。

  • 字符串: 表示一个绝对路径或者相对路径。例如:

    <router-link to="/users/123">User Profile</router-link>
    <router-link to="profile">My Profile</router-link>
  • 对象: 允许我们更灵活地指定路由参数。例如:

    <router-link :to="{ path: '/users', query: { page: 2 } }">Users</router-link>
    <router-link :to="{ name: 'user', params: { id: 123 } }">User Profile</router-link>

4. replace 属性:替换历史记录

replace 属性决定了导航的方式。如果 replacetrue,则会调用 router.replace 方法,替换浏览器的历史记录。否则,会调用 router.push 方法,添加一条新的历史记录。

5. active-classexact-active-class:激活状态的样式

active-classexact-active-class 属性用于指定当 RouterLink 处于激活状态时,要添加的 CSS 类名。active-class 会在当前路由是目标路由的子路由时生效,exact-active-class 会在当前路由与目标路由完全匹配时生效。

6. custom 属性:自定义渲染

custom 属性允许我们完全自定义 RouterLink 的渲染方式。当 customtrue 时,RouterLink 不会直接渲染一个 <a> 标签,而是将 hrefnavigate 函数作为 props 传递给插槽,由用户自定义渲染。

例如:

<router-link to="/about" v-slot="{ href, navigate, route }" custom>
  <button :href="href" @click.prevent="navigate">
    Go to {{ route.path }}
  </button>
</router-link>

在这个例子中,我们使用 custom 属性,将 hrefnavigate 函数传递给插槽,然后在插槽中使用一个 <button> 元素来渲染链接。

总结:RouterLink 是路由导航的核心,它通过 to 属性指定导航目标,通过 replace 属性控制导航方式,通过 active-classexact-active-class 属性控制激活状态的样式,通过 custom 属性支持自定义渲染。

特性 描述
核心职责 创建可以触发路由变化的导航链接
to 属性 指定导航的目标路由,可以是字符串或者对象
replace 属性 决定导航的方式,true 表示替换历史记录,false 表示添加新的历史记录
active-classexact-active-class 用于指定当 RouterLink 处于激活状态时,要添加的 CSS 类名
custom 属性 允许完全自定义 RouterLink 的渲染方式,将 hrefnavigate 函数传递给插槽

第三部分:RouterViewRouterLink 如何与路由实例交互

RouterViewRouterLink 都需要与 Vue Router 的路由实例进行交互,才能实现路由的渲染和导航。

  • RouterView: 通过 inject API,从父级组件(通常是 Router 组件或者其他的 RouterView)注入路由信息。这些信息包括当前路由对象 (route)、路由匹配结果 (matched) 等。RouterView 根据这些信息,决定要渲染哪个组件。

  • RouterLink: 通过 useRouter composable 获取 router 实例。RouterLink 使用 router.resolve 方法解析 to 属性,得到一个包含 resolvedhref 的对象。当用户点击 RouterLink 时,它会调用 router.push 或者 router.replace 方法,更新浏览器的 URL,并触发 RouterView 的重新渲染。

可以用一个表格简单总结:

组件 交互方式 作用
RouterView inject 从父级组件获取路由信息(当前路由对象、路由匹配结果等) 根据路由信息,决定要渲染哪个组件
RouterLink useRouter 获取 router 实例;使用 router.resolve 解析 to 属性;调用 router.push 或者 router.replace 方法 创建导航链接,当用户点击链接时,更新浏览器的 URL,并触发 RouterView 的重新渲染

第四部分:总结与升华

今天我们深入剖析了 Vue Router 中的 RouterViewRouterLink 组件。我们了解了它们的核心职责、源码实现,以及它们如何与路由实例交互。

  • RouterView 负责根据当前路由渲染对应的组件,是路由渲染的核心。
  • RouterLink 负责创建导航链接,是路由导航的核心。

这两个组件相互配合,共同完成了 Vue Router 的路由功能。

希望今天的分享对你有所帮助。记住,理解框架的底层原理,才能更好地使用框架,写出更健壮、更高效的代码。

下次有机会,我们再聊聊 Vue Router 的其他部分,比如路由匹配算法、导航守卫等等。

感谢大家的聆听!下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注