50行书写一个router-view组件

149 阅读3分钟

router-view组件是做什么的

router-view 是 Vue Router 的核心组件,就是根据当前路由路径动态渲染匹配的组件

我们写一个先写一个router的配置。

import { createRouter, createWebHistory } from '../mini-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
import AView from '../views/AView.vue'
import BView from '../views/BView.vue'
import CView from '../views/CView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView,
      children: [
        {
          path: 'a',
          component: AView
        },
        {
          path: 'b',
          component: BView
        },
        {
          path: 'c',
          component: CView
        }
      ]
    },
    {
      path: '/about',
      name: 'about',
      component: AboutView
    }
  ]
})

export default router

假设目前路径为: /a

<!-- 父组件模板 -->
<template>
  <!-- 深度0:渲染根路径(/)的组件 -->
  <router-view>   <!-- 渲染 HomeView -->
    <!-- 深度1:渲染嵌套路由(/a)的组件 -->
    <router-view/> <!-- 渲染 AView -->
  </router-view>
</template>

那么第一个router-view组件就加载 /路径的组件

第二个router-view组件就加载/a 路径对应的组件

把这件事件请说的在公式一些就是

/a路径可以匹配到两个组件 / 和 /a,写成数组就是

// 路径 /a 的匹配结果
[
  { path: '/', component: HomeView },  // 深度0
  { path: '/a', component: AView }    // 深度1
]

所以第一个router-view的深度为0,第二个router-view的深度为1,如果还有嵌套那么第三个router-view的深度就为3。

然后每个router-view组件的深度就是他需要加载的组件在数组中的下标

所以我们要做到根据路径加载对应组,我们就需要完成以下几件事情。

  1. 动态渲染某个组件
  2. 根据路径获取到这样一个数组
    // 路径 /a 的匹配结果
    [
      { path: '/', component: HomeView },  // 深度0
      { path: '/a', component: AView }    // 深度1
    ]
    
  3. 每个router-view都需确定其深度。

确定了这三个条件之后,就可以根据router-view的深度,去匹配的数组中找到对应组件进行渲染即可。

思路有了,我们就开实现每个条件。

  1. 动态渲染,可以使用component组件或者h函数返回虚拟dom,这里就使用h函数了。
  2. 匹配这里就直接使用内置的方法了。
  3. 对于深度,我们只需要知道父组件的深度,然后 + 1就是自己组件的深度,肯定没办法使用prop传递给子组件自己的深度,因为我们没有办法直接找到对应的子router-view组件,但是router-view既然是自己的子组件,所以我们可以使用provide传值。

具体实现

import { inject, computed, type Slots, provide, h } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'

export const RouterView = {
  name: 'RouterView',
  setup(_: unknown, { slots }: { slots: Slots }) {
    // 当我们进行组件渲染的时候,这里会存在一个层级的关系
    // 因为 matched 是一个有层次关系(父子关系)的组件数组,意味着 router-view 可能存在好几个
    // 所以这里存在一个“深度”的问题

    // 这里可以通过 inject 的方法,从父组件那里去获取 depth
    // 根据 depth 来得知当前的 router-view 是第几层
    // 如果父组件没有提供 depth,说明这是最顶层的 router-view,我们给一个默认值 0
    const depth: number = inject('depth', 0)


    const injectRoute: RouteLocationNormalizedLoaded = inject('route location')!

    // 获取对应层数的匹配上的路由记录
    const matchedRouteRef = computed(() => injectRoute.matched[depth])

    // 第一次执行完成后,我们要使其 +1
    // 把这些信息提供给下一层
    provide('depth', depth + 1)

    // 返回一个渲染函数,这个函数在每次重新渲染的时候会被调用
    return () => {
      // 获取对应的路由记录
      const matchRoute = matchedRouteRef.value
      // 从路由记录上面获取到组件
      const viewComponent = matchRoute && matchRoute?.components?.default

      // 查看组件是否存在
      if (!viewComponent) {
        // 进入到此 if,说明没有找到对应的组件
        // 那么就渲染默认的插槽(如果有的话)
        return slots.default && slots.default()
      }

      // 找到了对应的组件
      // 渲染该组件
      return h(viewComponent)
    }
  }
}