VueRouter 原理解读之初始化流程

Vue
279
0
0
2023-05-22
目录
  • 1.1 核心概念
  • 官方介绍
  • 使用与阅读源码的必要性
  • 1.2 基本使用
  • 路由配置与项目引入
  • 路由组件使用
  • 跳转 api 调用
  • 2.1 createRouter 初始化入口分析
  • 大致流程
  • Router 对象的定义:
  • 创建路由流程概括
  • 2.2 创建页面路由匹配器
  • 2.3 创建初始化导航守卫
  • useCallbacks 实现订阅发布中心
  • 创建相关的导航守卫
  • 2.4 定义挂载相关 Router 方法
  • 路由配置相关 addRoute、removeRoute、hasRoute、getRoutes
  • 路由操作相关 push、replace、go、back、forward
  • 钩子相关 beforeEach、beforeResolve、afterEach、onError
  • 注册安装 install 方法:
  • 总结:

1.1 核心概念

官方介绍

Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。功能包括:

  • 嵌套路由映射
  • 动态路由选择
  • 模块化、基于组件的路由配置
  • 路由参数、查询、通配符
  • 展示由 Vue.js 的过渡系统提供的过渡效果
  • 细致的导航控制
  • 自动激活 CSS 类的链接
  • HTML5 history 模式或 hash 模式
  • 可定制的滚动行为
  • URL 的正确编码

使用与阅读源码的必要性

现代工程化的前端项目只要使用到 Vue.js 框架,基本都是逃离不了如何对 SPA 路由跳转的处理,而 VueRouter 作为一个成熟、优秀的前端路由管理库也是被业界广泛推荐和使用,因此对 Vue 开发者来讲,深入底层的了解使用 VueRouter ,学习其实现原理是很有必要的。随着不断对 VueRouter 的深度使用,一方面就是在实践当中可能遇到一些需要额外定制化处理的场景,像捕获一些异常上报、处理路由缓存等场景需要我们对 VueRouter 有着一定程度的熟悉才能更好的处理;另一方面则是业余外的学习、拓展自身能力的一个渠道,通过对源码的阅读理解能够不断开拓自己的知识面以及提升自己的 CR 水平还有潜移默化当中的一些技术设计、架构能力等。

1.2 基本使用

路由配置与项目引入

// 1. 定义相关路由视图组件
const Home = { template: '<div>Home</div>' }
const About = { template: '<div>About</div>' }
// 2. 定义相关路由路径等路由配置
const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]
// 3. 通过 createRouter 方法传入路由配置参数进行路由对象创建
const router = VueRouter.createRouter({
  // 4. 选择路由能力的模式进行初始化创建
  history: VueRouter.createWebHashHistory(),
  routes,
})
// 5. 调用 Vue.js 对象的 use 方法来对 VueRouter 路由对象进行初始化安装处理
const app = Vue.createApp({})
app.use(router)
app.mount('#app')

路由组件使用

<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content">
  <router-link to="/" reaplace>Home</router-link>
</router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>

跳转 api 调用

export default {
  methods: {
    redirectHome() {
      this.$router.replace('/')
    },
    goToAbout() {
      this.$router.push('/about')
    },
  },
}

当然 VueRouter 的能力和相关的 api 肯定不仅仅是这块基础使用这么简单,具体其他相关更高级、深层次的 api 与用法请参考官方文档等。因为是 VueRouter 源码分析和原理解析的系列文章,受众最好是有一定的使用经验的开发者甚至是深度使用者更好,因此可能会存在一点门槛,这块需要阅读者自行斟酌。

2.1 createRouter 初始化入口分析

大致流程

createRouter这个方法的代码简单化后如下:能够看到createRouter方法在内部定义了一个 router 对象并在这个对象上挂载一些属性与方法,最后将这个 router 对象作为函数返回值进行返回。

// vuejs:router/packages/router/src/router.ts
export function createRouter(options: RouterOptions): Router {
  const matcher = createRouterMatcher(options.routes, options)
  const parseQuery = options.parseQuery || originalParseQuery
  const stringifyQuery = options.stringifyQuery || originalStringifyQuery
  const routerHistory = options.history
  const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const afterGuards = useCallbacks<NavigationHookAfter>()
  const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(START_LOCATION_NORMALIZED)
  let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
  function addRoute(parentOrRoute: RouteRecordName | RouteRecordRaw, route?: RouteRecordRaw) {
    // ··· ···
  }
  function removeRoute(name: RouteRecordName) {
    // ··· ···
  }
  function getRoutes() {
    // ··· ···
  }
  function hasRoute(name: RouteRecordName): boolean {
    // ··· ···
  }
  function resolve(rawLocation: Readonly<RouteLocationRaw>, currentLocation?: RouteLocationNormalizedLoaded): RouteLocation & { href: string } {
    // ··· ···
  }
  function push(to: RouteLocationRaw) {
    // ··· ···
  }
  function replace(to: RouteLocationRaw) {
    // ··· ···
  }
  let readyHandlers = useCallbacks<OnReadyCallback>()
  let errorHandlers = useCallbacks<_ErrorHandler>()
  function isReady(): Promise<void> {
    // ··· ···
  }
  const go = (delta: number) => routerHistory.go(delta)
  const router: Router = {
    // ··· ···
  }
  return router
}

Router 对象的定义:

从上面的createRouter方法定义当中能够知道,返回的是一个 Router 的对象,我们首先来看下 Router 对象的属性定义:返回项 Router 是创建出来的全局路由对象,包含了路由的实例和常用的内置操作跳转、获取信息等方法。

// vuejs:router/packages/router/src/router.ts
export interface Router {
  // 当前路由
  readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
	// VueRouter 路由配置项
  readonly options: RouterOptions
  // 是否监听中
  listening: boolean
  // 动态增加路由项
  addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
  addRoute(route: RouteRecordRaw): () => void
  // 动态删除路由项
  removeRoute(name: RouteRecordName): void
  // 根据路由配置的 name 判断是否有该路由
  hasRoute(name: RouteRecordName): boolean
  // 获取当前所有路由数据
  getRoutes(): RouteRecord[]
  // 当前网页的标准路由 URL 地址
  resolve(
    to: RouteLocationRaw,
    currentLocation?: RouteLocationNormalizedLoaded
  ): RouteLocation & { href: string }
  // 路由导航跳转操作方法
  push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
  replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
  back(): ReturnType<Router['go']>
  forward(): ReturnType<Router['go']>
  go(delta: number): void
  // 全局守卫
  beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
  beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
  afterEach(guard: NavigationHookAfter): () => void
  // 路由错误回调处理
  onError(handler: _ErrorHandler): () => void
  // 路由是否已经完成初始化导航
  isReady(): Promise<void>
  // 2.x 版本的 Vue.js 引入 VueRouter 时候自动调用
  install(app: App): void
}

创建路由流程概括

createRouter的方法当中,我们能够看到该方法其实主要是做了三件事情,

  • 使用createRouterMatcher创建页面路由匹配器;
  • 创建和处理守卫相关方法;
  • 定义其他相关的 router 对象的属性和内置的方法。

接下来我们来具体分析里面的三个大步骤到底分别处理做了些什么事情呢。

2.2 创建页面路由匹配器

在前面的简单分析当中,在createRouter的第一步就是根据配置的路由 options 配置调用createRouterMacher方法创建页面路由匹配器matcher对象。

// vuejs:router/packages/router/src/matcher/index.ts
export function createRouterMatcher(
    routes: Readonly<RouteRecordRaw[]>,
    globalOptions: PathParserOptions
): RouterMatcher {
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
  globalOptions = mergeOptions(
      { strict: false, end: true, sensitive: false } as PathParserOptions,
      globalOptions
  )
  function getRecordMatcher(name: RouteRecordName) {
    return matcherMap.get(name)
  }
  function addRoute(
      record: RouteRecordRaw,
      parent?: RouteRecordMatcher,
      originalRecord?: RouteRecordMatcher
  ) {
    // ··· ···
  }
  function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
    // ··· ···
  }
  function getRoutes() {
    // ··· ···
  }
  function resolve(location: Readonly<MatcherLocationRaw>, currentLocation: Readonly<MatcherLocation>): MatcherLocation {
    // ··· ···
  }
  routes.forEach(route => addRoute(route))
  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

对于 VueRouter 整个库来看这个页面路由匹配器matcher对象是占据了比较大的一个模块,这篇文章主要是对路由初始化这部分流程逻辑进行分析;也因为篇幅的原因,这里就先简单从较上帝的视角来看看这个大概的流程。

  • 方法内声明了matchersmatcherMap两个内部变量存放经过解析的路由配置信息;
  • 创建相关的路由匹配器的操作方法:addRoute, resolve, removeRoute, getRoutes, getRecordMatcher;
  • 根据调用createRouter方法传入的参数遍历调用addRoute初始化路由匹配器数据;
  • 最后方法返回一个对象,并且将该些操作方法挂载到该对象属性当中;

2.3 创建初始化导航守卫

useCallbacks 实现订阅发布中心

// vuejs:router/packages/router/src/utils/callbacks.ts
export function useCallbacks<T>() {
  let handlers: T[] = []
  function add(handler: T): () => void {
    handlers.push(handler)
    return () => {
      const i = handlers.indexOf(handler)
      if (i > -1) handlers.splice(i, 1)
    }
  }
  function reset() {
    handlers = []
  }
  return {
    add,
    list: () => handlers,
    reset,
  }
}

这里首先简单分析下useCallbackhooks 的方法,其实就是利用闭包创建一个内部的回调函数数组变量,然后再创建和返回一个对象,对象有三个属性方法,分别是add添加一个回调执行函数并且返回一个清除当前回调函数的一个函数,list获取回调函数数组,reset清空当前所有回调方法。是一个简单的标准的发布订阅中心处理的实现。

创建相关的导航守卫

// vuejs:router/packages/router/src/router.ts
import { useCallbacks } from './utils/callbacks'
export function createRouter(options: RouterOptions): Router {
  const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const afterGuards = useCallbacks<NavigationHookAfter>()
  // ··· ···
  const router: Router = {
    // ··· ···
    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,
    // ··· ···
  }
  return router
}

通过上面经过节选的相关导航守卫处理的部分代码,能够看到其实 VueRouter 在createRouter里面对于全局导航守卫的处理还是比较简单通俗易懂的,通过useCallbackshooks 方法分别创建了beforeEachbeforeResolveafterEach三个对应的全局导航守卫的回调处理对象(这里主要是初始化创建相关的订阅发布的发布者对象);

  • beforeEach:在任何导航路由之前执行;
  • beforeResolve:在导航路由解析确认之前执行;
  • afterEach:在任何导航路由确认跳转之后执行;

因为篇幅问题,VueRouter 的守卫其实不仅仅这些,后面会梳理整理相关的守卫处理,订阅回调的发布执行等相关逻辑作为一篇守卫相关的文章单独编写,这里就不讲述过多的东西了。

2.4 定义挂载相关 Router 方法

在 router 对象上面还挂载了不少方法,接下来我们来简单分析下这些方法的实现逻辑。

路由配置相关 addRoute、removeRoute、hasRoute、getRoutes

// vuejs:router/packages/router/src/router.ts
export function createRouter(options: RouterOptions): Router {
	// ··· ···
  // 添加路由项 - 兼容处理参数后使用 addRoute 进行添加路由项
  function addRoute(
    parentOrRoute: RouteRecordName | RouteRecordRaw,
    route?: RouteRecordRaw
  ) {
    let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
    let record: RouteRecordRaw
    if (isRouteName(parentOrRoute)) {
      parent = matcher.getRecordMatcher(parentOrRoute)
      record = route!
    } else {
      record = parentOrRoute
    }
    return matcher.addRoute(record, parent)
  }
  // 删除路由项 - 根据路由名 name 调用 getRecordMatcher 获取路由项,如果找到记录则调用 removeRoute 删除该路由项
  function removeRoute(name: RouteRecordName) {
    const recordMatcher = matcher.getRecordMatcher(name)
    if (recordMatcher) {
      matcher.removeRoute(recordMatcher)
    }
  }
  // 获取当前所有路由项 - 
  function getRoutes() {
    return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
  }
  // 是否含有路由 - 根据路由名 name 调用 getRecordMatcher 获取路由项
  function hasRoute(name: RouteRecordName): boolean {
    return !!matcher.getRecordMatcher(name)
  }
  const router: Router = {
    addRoute,
    removeRoute,
    hasRoute,
    getRoutes,
  }
  return router
}

这部分是对路由配置的操作方法的实现,但是看下来逻辑并不难,都是比较清晰。在前面的章节当中我们对页面路由匹配器matcher进行了简单的分析,知道了在createRouterMacher方法返回的这个对象包含着 addRoute, resolve, removeRoute, getRoutes, getRecordMatcher 这些操作方法,并且内部维护着路由匹配器的信息。

这部分路由操作方法就是利用这个createRouterMacher所创建的页面路由匹配器matcher挂载的方法来实现的。

路由操作相关 push、replace、go、back、forward

// vuejs:router/packages/router/src/router.ts
export function createRouter(options: RouterOptions): Router {
  const routerHistory = options.history
  function push(to: RouteLocationRaw) {
    return pushWithRedirect(to)
  }
  function replace(to: RouteLocationRaw) {
    return push(assign(locationAsObject(to), { replace: true }))
  }
  const go = (delta: number) => routerHistory.go(delta)
  const router: Router = {
    push,
    replace,
    go,
    back: () => go(-1),
    forward: () => go(1),
  }
  return router
}

先来看比较简单的gobackforward这几个方法,这几个方法都是直接调用路由历史对象的go方法,底层其实就是调用浏览器的 history 提供的 go 跳转 api,这个路由跳转的会在另外的专门讲解路由模式的文章当中讲述,这里就不展开详细讲述了。

而另外的pushreplace方法能从上面看到replace其实就是调用push的方法,都是使用pushWithRedirect处理跳转,仅一个 replace 的参数不同。接着我们来分析pushWithRedirect这个方法。

// vuejs:router/packages/router/src/router.ts
function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
  // 定义相关的路由变量属性
  const targetLocation: RouteLocation = (pendingLocation = resolve(to))
  const from = currentRoute.value
  const data: HistoryState | undefined = (to as RouteLocationOptions).state
  const force: boolean | undefined = (to as RouteLocationOptions).force
  const replace = (to as RouteLocationOptions).replace === true
  // 调用 handleRedirectRecord 判断目标跳转是否需要重定向 -- 就是这个 to 要跳转的路由的 redirect 属性是否为 true
  const shouldRedirect = handleRedirectRecord(targetLocation)
  // 若需要重定向则递归调用 pushWithRedirect 方法
  if (shouldRedirect)
    return pushWithRedirect(
      assign(locationAsObject(shouldRedirect), {
        state:
          typeof shouldRedirect === 'object'
          ? assign({}, data, shouldRedirect.state)
          : data,
        force,
        replace,
      }),
      redirectedFrom || targetLocation
    )
	// 后续逻辑是非重定向的路由
  const toLocation = targetLocation as RouteLocationNormalized
  toLocation.redirectedFrom = redirectedFrom
  let failure: NavigationFailure | void | undefined
  // 不设置强制跳转并且目标跳转路由地址与当前路由地址一样的情况下定义相关的跳转异常以及页面的滚动,后续使用 Promise.resolve 处理异常
  if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
    failure = createRouterError<NavigationFailure>(
      ErrorTypes.NAVIGATION_DUPLICATED,
      { to: toLocation, from }
    )
    handleScroll(from, from, true, false)
  }
  // 判断前面的执行逻辑是否存在跳转异常或者错误,如果没有跳转异常错误则执行 navigate 这个Promise方法
  return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
    .catch((error: NavigationFailure | NavigationRedirectError) => {
      // 处理跳转中出现异常后捕获相关的错误并对不同错误进行处理
      isNavigationFailure(error)
        ? isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT) ? error : markAsReady(error)
        : triggerError(error, toLocation, from) 
    }).then((failure: NavigationFailure | NavigationRedirectError | void) => {
      // 跳转调用 navigate 后的处理
      if (failure) {
        // 处理跳转和执行navigate过程当中的错误异常
      } else {
        failure = finalizeNavigation(
          toLocation as RouteLocationNormalizedLoaded,
          from,
          true,
          replace,
          data
        )
      }
      triggerAfterEach(toLocation as RouteLocationNormalizedLoaded, from, failure)
      return failure
    })
}

在对pushWithRedirect方法进行分析后知道这个方法是对页面的重定向进行专门处理,处理完成后会调用navigate这个 Promise 方法。

pushWithRedirect方法的逻辑末尾中,一系列的逻辑处理完成后才会调用finalizeNavigationtriggerAfterEach进行导航切换路由的确认与相关导航守卫钩子的收尾执行。

我们先来看下这个navigate方法的逻辑:

// vuejs:router/packages/router/src/router.ts
function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
  let guards: Lazy<any>[]
  // extractChangingRecords 方法会根据(to/目标跳转路由)和(from/离开的路由)到路由匹配器matcher里匹配对应的路由项并且将结果存到3个数组中
  //		leavingRecords:当前即将离开的路由
  //		updatingRecords:要更新的路由
  //		enteringRecords:要跳转的目标路由
  const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)
  // extractComponentsGuards 方法用于提取不同的路由钩子,第二个参数可传值:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
  guards = extractComponentsGuards(
    leavingRecords.reverse(), // 这里因为Vue组件销毁顺序是从子到父,因此使用reverse反转数组保证子路由钩子顺序在前
    'beforeRouteLeave',
    to,
    from
  )
  // 将失活组件的 onBeforeRouteLeave 导航守卫都提取并且添加到 guards 里
  for (const record of leavingRecords) {
    record.leaveGuards.forEach(guard => {
      guards.push(guardToPromiseFn(guard, to, from))
    })
  }
  // 检查当前正在处理的目标跳转路由和 to 是否相同路由,如果不是的话则抛除 Promise 异常
  const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from)
  guards.push(canceledNavigationCheck)
  return (
    runGuardQueue(guards) // 作为启动 Promise 开始执行失活组件的 beforeRouteLeave 钩子
    .then(() => {
      // 执行全局 beforeEach 钩子
      guards = []
      for (const guard of beforeGuards.list()) {
        guards.push(guardToPromiseFn(guard, to, from))
      }
      guards.push(canceledNavigationCheck)
      return runGuardQueue(guards)
    })
    .then(() => {
      // 执行重用组件的 beforeRouteUpdate 钩子
      guards = extractComponentsGuards(
        updatingRecords,
        'beforeRouteUpdate',
        to,
        from
      )
      for (const record of updatingRecords) {
        record.updateGuards.forEach(guard => {
          guards.push(guardToPromiseFn(guard, to, from))
        })
      }
      guards.push(canceledNavigationCheck)
      return runGuardQueue(guards)
    })
    .then(() => {
      // 执行全局 beforeEnter 钩子
      guards = []
      for (const record of to.matched) {
        if (record.beforeEnter && !from.matched.includes(record)) {
          if (isArray(record.beforeEnter)) {
            for (const beforeEnter of record.beforeEnter)
              guards.push(guardToPromiseFn(beforeEnter, to, from))
          } else {
            guards.push(guardToPromiseFn(record.beforeEnter, to, from))
          }
        }
      }
      guards.push(canceledNavigationCheck)
      return runGuardQueue(guards)
    })
    .then(() => {
      // 清除已经存在的 enterCallbacks, 因为这些已经在 extractComponentsGuards 里面添加
      to.matched.forEach(record => (record.enterCallbacks = {}))
      // 执行被激活组件的 beforeRouteEnter 钩子
      guards = extractComponentsGuards(
        enteringRecords,
        'beforeRouteEnter',
        to,
        from
      )
      guards.push(canceledNavigationCheck)
      return runGuardQueue(guards)
    })
    .then(() => {
      // 执行全局 beforeResolve 钩子
      guards = []
      for (const guard of beforeResolveGuards.list()) {
        guards.push(guardToPromiseFn(guard, to, from))
      }
      guards.push(canceledNavigationCheck)
      return runGuardQueue(guards)
    })
    .catch(err =>
      // 处理在过程当中抛除的异常或者取消导航跳转操作
      isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
      ? err
      : Promise.reject(err)
          )
  )
}

这个navigate方法主要就是使用runGuardQueue封装即将要执行的相关的一系列导航守卫的钩子回调,这块封装的内部处理逻辑还是比较复杂的,这里因为篇幅问题我们这块还是以了解知道在这块主要的流程逻辑,后续也会对路由守卫这块专门详细的进行源码阅读分析并且编写相关的文章。

接着我们再来看下finalizeNavigation是如何进行前端路由跳转的:

// vuejs:router/packages/router/src/router.ts
function finalizeNavigation(
  toLocation: RouteLocationNormalizedLoaded,
  from: RouteLocationNormalizedLoaded,
  isPush: boolean,
  replace?: boolean,
  data?: HistoryState
): NavigationFailure | void {
  // 检查是否需要取消目标路由的跳转 -- 判断目标跳转的路由和当前处理的跳转路由是否不同,不同则取消路由跳转
  const error = checkCanceledNavigation(toLocation, from)
  if (error) return error
  const isFirstNavigation = from === START_LOCATION_NORMALIZED
  const state = !isBrowser ? {} : history.state
  if (isPush) {
    // 处理路由跳转,判断根据 replace 参数判断使用 replace 还是 push 的跳转形式
    if (replace || isFirstNavigation)
      routerHistory.replace(
        toLocation.fullPath,
        assign({ scroll: isFirstNavigation && state && state.scroll, }, data)
      )
    else routerHistory.push(toLocation.fullPath, data)
  }
  currentRoute.value = toLocation
  // 处理设置页面的滚动
  handleScroll(toLocation, from, isPush, isFirstNavigation)
  markAsReady()
}

在逻辑当中能够看到调用 VueRouter 的pushreplace方法进行跳转时候会调用这个routerHistory路由历史对象对应的同名 api 进行跳转处理,但是受限于文章的篇幅,这块先剧透这里底层路由跳转逻辑使用的是浏览器的historypushStatereplaceState这两个 api,后续很快就会推出相关的前端路由能力实现原理的剖析文章,大家敬请关注。

钩子相关 beforeEach、beforeResolve、afterEach、onError

// vuejs:router/packages/router/src/router.ts
import { useCallbacks } from './utils/callbacks'
export function createRouter(options: RouterOptions): Router {
  const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const afterGuards = useCallbacks<NavigationHookAfter>()
  let errorHandlers = useCallbacks<_ErrorHandler>()
  // ··· ···
  const router: Router = {
    // ··· ···
    // 抛出相关导航守卫或钩子对应的新增订阅回调事件
    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,
    onError: errorHandlers.add,
    // ··· ···
  }
  return router
}

这块钩子的大部分的逻辑已经在前面创建初始化导航守卫这个小章节里面已经讲述了,因此这块定义挂载抛出钩子事件的逻辑其实也较明朗了:

  • 使用useCallbacks方法定义相关钩子的订阅发布中心对象;
  • createRouter方法返回的 router 对象定义增加对应的订阅事件add,这样子在定义路由时候配置传入的钩子回调函数则自动被添加到对应钩子的订阅回调列表当中。

注册安装 install 方法:

熟悉 Vue.js 技术栈的同学都基本知道这个install方法会在插件库引入 Vue 项目当中的Vue.use方法当中被调用,这里 VueRouter 的 install 也不例外,同样会在下面的 use 方法的调用当中被 Vue.js 内部所调用到。

const app = Vue.createApp({})
app.use(router)

接下来我们真正进入到对install方法的分析当中去:

// vuejs:router/packages/router/src/router.ts
install(app: App) {
  const router = this
  // 注册 VueRouter 的路由视图和链接组件为 Vue 全局组件
  app.component('RouterLink', RouterLink)
  app.component('RouterView', RouterView)
  // 在全局 Vue this 对象上挂载 $router 属性为路由对象
  app.config.globalProperties.$router = router
  Object.defineProperty(app.config.globalProperties, '$route', {
    enumerable: true,
    get: () => unref(currentRoute),
  })
  // 判断浏览器环境并且还没执行初始化路由跳转时候先进行一次 VueRouter 的 push 路由跳转
  if (
    isBrowser &&
    !started &&
    currentRoute.value === START_LOCATION_NORMALIZED
  ) {
    started = true
    push(routerHistory.location).catch(err => {
      if (__DEV__) warn('Unexpected error when starting the router:', err)
    })
  }
  // 使用 computed 计算属性来创建一个记录当前已经被激活过的路由的对象 reactiveRoute
  const reactiveRoute = {} as {
    [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
     RouteLocationNormalizedLoaded[k]
     >
  }
  for (const key in START_LOCATION_NORMALIZED) {
    reactiveRoute[key] = computed(() => currentRoute.value[key])
  }
  // 全局注入相关的一些路由相关的变量
  app.provide(routerKey, router)
  app.provide(routeLocationKey, reactive(reactiveRoute))
  app.provide(routerViewLocationKey, currentRoute)
  // 重写覆盖 Vue 项目的卸载钩子函数 - 执行相关属性的卸载并且调用原本 Vue.js 的卸载 unmount 事件
  const unmountApp = app.unmount
  installedApps.add(app)
  app.unmount = function () {
    installedApps.delete(app)
    if (installedApps.size < 1) {
      pendingLocation = START_LOCATION_NORMALIZED
      removeHistoryListener && removeHistoryListener()
      removeHistoryListener = null
      currentRoute.value = START_LOCATION_NORMALIZED
      started = false
      ready = false
    }
    unmountApp() // 执行原本 Vue 项目当中设置的 unmount 钩子函数
  }
}

install方法当中主要逻辑还是对一些全局的属性和相关的组件、变量以及钩子事件进行一个初始化处理操作:

这块的逻辑可能有些操作在一开始初始化时候可能看不太懂为啥要这样处理,后面会继续推出 VueRouter 系列的源码解析,到时候会回来回顾这块的一些 install 引入安装路由库时候里面的一些操作与源码逻辑。

总结:

至此,VueRouter 这个前端路由库的初始化流程createRouter就简单的分析完成了,这篇初始化的源码解析的文章更多的像是领入门的流程概述简析。

虽然说初始化主要做了前面讲述的三个步骤:创建页面路由匹配器、导航守卫、初始化 router 对象并且返回。但是这三件事情当中其实还是有着不少的处理细节,里面还牵涉了不少其他功能模块的实现,一开始可能还只能大概通过上帝模式去俯瞰这个初始化的流程,可能仅仅留有个印象(这里因为篇幅问题未能够各个方面都进行很详细的讲解,后续也会沿着这些伏笔线索不断推出相关的源码原理解析文章),开篇反而可能存在着一定程度的心智负担。但是相信跟随着后面的一系列文章,应该能够将系统串联起来,对 VueRouter 的实现有更完整的认知。

相关的参考资料