vue-router 如何做到页面切换?<root-view>, <root-link> 源码解析

604 阅读4分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

写路由相关的代码的时候,写完页面组件的相关的代码后,就可以直接一把梭哈 vue-router,但是 vue-router 是怎么实现它们间的切换的呢?

不知道大家有没有写过 JSP,我曾经短暂的写过一个星期 JSP, 过程让我有点煎熬,JSP 的工程化没有现在的我们使用的 webpack 那么好,就比如热更新这一开,JSP 的更新是需要重启整个 SpringBoot! 很难受,但可以说一下的是 JSP 实现页面切换的功能其实写起来和 vue-router 也是差不多的

<!-- 有点命名视图的感觉了 -->
<jsp:include page="head.jsp"></jsp:include>
<html></html>
<jsp:include page="bottom.jsp"></jsp:include>
<router-view name="head"></router-view>
<router-view></router-view>
<router-view name="bottom"></router-view>

JSP 的页面切换就像你看得一样,点击后返回目录中的 .jsp 文件,那么 vue-router 是怎么去做的呢?

<router-link>

<router-link> 其实没什么好讲的,源码小 300 行,让我们浅尝一下源码,router/src/RouterLink.ts 184

export const RouterLinkImpl = /*#__PURE__*/ defineComponent({
  // ...
  setup(props, { slots }) {
    const link = reactive(useLink(props));
    const { options } = inject(routerKey)!;

    // 样式计算
    // ...

    return () => {
      const children = slots.default && slots.default(link);
      return props.custom
        ? children
        : h(
            "a",
            {
              "aria-current": link.isExactActive
                ? props.ariaCurrentValue
                : null,
              href: link.href,
              // this would override user added attrs but Vue will still add
              // the listener so we end up triggering both
              onClick: link.navigate,
              class: elClass.value,
            },
            children
          );
    };
  },
});

简单来说就是返回一个 <a>, 里面包含你要跳转的路由(即 URL),说过题外话,像这种库一般都是用 render 函数去渲染组件的(就是上面那个 h()),而不是像我们平时用的模板语法 <template>, 什么叫函数式组件?这就叫函数式组件

注意 <router-link> 有个小细节,看上面那个 onClick 就是拦截 <a> 原有的跳转事件,为什么这么做?因为 vue-router4 采用的是 history 模式,不同于 hash 模式 URL 前面挂个 # 就随便你怎么改都不会发送到服务器,history 模式就需要把跳转给拦截不然就跳到 404 了,下面有个例子

https://www.baidu.com/xxx  // 这个访问会报错
https://www.baidu.com/#xxx // 这个就不会

再说一个小细节,vue-router4 是默认拦截的,因为 vue-router4 的 hash 模式不是正经 hash, 其底层也是用的 history 模式

<router-view>

注意注意 <router-view> 才是页面切换的敲门砖,但 <router-view> 的源码比 <router-link> 要少,有 231 行,不过核心都是一样的,router/src/RouterView.ts 43(我们还是挑最核心的部分来看)

export const RouterViewImpl = /*#__PURE__*/ defineComponent({
  setup(props, { attrs, slots }) {
    const injectedRoute = inject(routerViewLocationKey)!
    const routeToDisplay = computed(() => props.route || injectedRoute.value)
    const depth = inject(viewDepthKey, 0)
    // 获得匹配路由
    const matchedRouteRef = computed<RouteLocationMatched | undefined>(
      () => routeToDisplay.value.matched[depth]
    )

    // 传递给子 router-view 它自己的路由层级
    provide(viewDepthKey, depth + 1)
    provide(matchedRouteKey, matchedRouteRef)
    provide(routerViewLocationKey, routeToDisplay)

    return () => {
      const route = routeToDisplay.value
      const matchedRoute = matchedRouteRef.value
      const ViewComponent = matchedRoute && matchedRoute.components[props.name]
      const currentName = props.name

      if (!ViewComponent) {
        return normalizeSlot(slots.default, { Component: ViewComponent, route })
      }

      const component = h(
        ViewComponent,
        assign({}, routeProps, attrs, {
          onVnodeUnmounted,
          ref: viewRef,
        })
      )
    }
  },
})

小细节小细节,<router-view> 深度迎合套娃性质,制定了一套 provide/inject 的父子 <router-view> 传递信息规则,通过父 <router-view> 传递下来的信息,使用 vue-router4 的 matcher 去找到正确的路由组件,没错,所谓的页面切换就是找组件的一个过程,对于一个个分散开来的 vue 组件,通过 matched 去找到组件,没错今天的主角就是 matcher!

又是一个小细节

matcher

matcher 其实是 vue-router 里面的核心,它有一个专门的文件夹 router/src/matcher,通过这个 matcher, vue-router 将从我们每一次路由导航的点击当中找到真正的路由组件

来到 router/src/router.ts 356

export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  ...
}

再配合一点官方文档的小知识

const Home = { template: "<div>Home</div>" };
const About = { template: "<div>About</div>" };

// options.routes 就是这里的 routes
const routes = [
  { path: "/", component: Home },
  { path: "/about", component: About },
];

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes,
});

createRouterMatcher() 的目的是为了创建我们定义的路由 routes 对应的映射表,而 <router-view> 就是通过这个映射表找到相应的组件并渲染

总结

vue-router4 页面切换

  1. Matcher 构建映射表
  2. <router-link> 拦截 <a> 跳转, 传递信息到 history 监听器
  3. <router-link> 根据 provide/inject 的信息, 找到映射表中的组件并渲染

这是写的关于 vue-router 的第三篇文章,基本上 vue-router4 的核心源码都看完了,但感觉家人们好像不是很喜欢看源码一类的文章,其实像 vue-router 这种我们写 vue 项目时几乎是必然要用的库,有机会还是可以去看一下的,源码并没有我一开想象的难,尤其 vue-router4 是用 typescript 写的,相较于传统的 JS 写的库,git clone 下来一堆报错,而 vue-router4 就不会,而且 vs-code 还提供了对 typescirpt 的分析功能,很方便就能够找到 vue-router4 中的某个方法的定义,某个变量的声明(当然 vs-code 做得还不够好,改天我会用 webstorm 去看一下),所以看源码难的是开始的决心