vue router 4 源码篇:导航守卫该如何设计(二)

4,021 阅读7分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

源码专栏

感谢大家继续阅读《Vue Router 4 源码探索系列》专栏,你可以在下面找到往期文章:

开场

哈喽大咖好,我是跑手,本次给大家继续探讨vue-router@4.x源码中有关导航守卫中,组件内守卫部分内容。假如你想更全面了解整个导航守卫的来龙去脉,建议你先阅读《导航守卫该如何设计(一)》这篇文章。

可获得的增益

在这章节中,你可以更系统并全面学习vue router的路由拦截模式和守卫设计模式,并可获得以下增益:

  1. 全面了解导航守卫核心源码;
  2. 掌握导航守卫设计模式;
  3. 组件内守卫的执行过程;

知识回顾

我们先回顾下vue-router@4.x中导航守卫的基本概念。

导航守卫分类

image.png

总的来讲,vue-router@4.x的导航守卫可以分三大类:

  1. 全局守卫:挂载在全局路由实例上,每个导航更新时都会触发。
  2. 路由独享守卫:挂载在路由配置表上,当指定路由进入时触发。
  3. 组件内守卫:定义在vue组件中,当加载或更新指定组件时触发。

完整的导航解析流程

image.png

解析:

  1. 首先是vue-router的history监听器监致使导航被触发,触发形式包括但不局限于router.pushrouter.replacerouter.go等等。
  2. 调用全局的 beforeEach 守卫,开启守卫第一道拦截。
  3. 审视新组件,判断新旧组件一致时(一般调用replace方法),先执行步骤2,再调用组件级钩子beforeRouteUpdate拦截。
  4. 若新旧组件不一致时,先执行步骤2,再调用路由配置表中的beforeEnter钩子进行拦截。
  5. 接下来在组件beforeCreate周期调用组件级beforeRouteEnter钩子,在组件渲染前拦截。
  6. 执行解析守卫 beforeResolve
  7. 在导航被确认后,就是组件的this对象生成后,可以使用全局的 afterEach 钩子拦截。
  8. 触发 DOM 更新。
  9. 销毁组件前(执行unmounted),会调用beforeRouteLeave 守卫进行拦截。

本章讨论的组件内守卫,涉及上面相关步骤3、5、9

源码解析

执行机制

在上篇文章讲到,vue-router中所有守卫执行过程都遵循以下原则:

  1. 先通过 guardToPromiseFn 方法将回调方法封装成Promise,以便后续链式调用。
  2. 再执行 runGuardQueue 方法,调用步骤1生产的Promise,完成导航守卫拦截。

组件内守卫与其他守卫的区别

虽说都是通过上述2步完成守卫拦截,但是在封装Promise前,组件内守卫和其他守卫还是有所区别。这在于,guardToPromiseFn接受的第一个参数(自定义的回调逻辑),组件内守卫必须先调用extractComponentsGuards将定义在vue组件内的守卫钩子提取出来,而其他守卫则省去这步。

extractComponentsGuards

  • 定义:提取vue组件内的守卫钩子函数。
  • 入参(4个,必填项):
    • matched:(从toform提取出来的路由记录,为以下三者之一:leavingRecords「即将离开的路由」、updatingRecords「即将更新的路由」、enteringRecords「即将进入的路由」)
    • guardType:守卫类型(beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
    • to:进入的路由
    • from:离开的路由
  • 返回:守卫钩子函数

执行过程:

image.png

函数内有2层for循环,外层循环是对入参matched的遍历,主要作用保证leavingRecords, updatingRecordsenteringRecords的所有record都得到处理; 内层循环对某record里面所有组件遍历,在这个循环中:

  1. 在开发环境下,程序帮忙做了一些容错措施。先对组件类型合法性判断,不为objectfunction类型则直接退出,避免页面崩溃;接下来是对 import('./component.vue') 方式引入的组件,会使用Promise函数包装;最后, 程序对defineAsyncComponent()方式定义的组件打标签,方便后续的区分处理。

  2. 接下来是守卫类型的判断,因为在3个组件内导航守卫中,只有beforeRouteEnter允许在组件mounted之前执行,当出现另外2个并且组件未挂载好时,要终止守卫的插入;

  3. 然后读取同步组件异步组件守卫钩子,对里面的守卫逻辑进行提取,值得注意的是这两部分代码都支持了vue-class-component方式构建的组件;

  4. 在同步组件中,先根据guardType读取要提取的导航,再通过guardToPromiseFn方法直接转化成Promise调用链放到守卫列表中保存;

  5. 在异步组件中,当组件加载完成(即组件函数Promise状态为fulfilled时),按步骤4的逻辑再执行一遍得到最终的调用链。关于Promise.resolve出来的结果如下图:

    image.png

  6. 返回整个守卫列表,流程结束。

源码:

export function extractComponentsGuards(
  matched: RouteRecordNormalized[],
  guardType: GuardType,
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
) {
  const guards: Array<() => Promise<void>> = []

  for (const record of matched) {
    // 为空组件则提示警告
    if (__DEV__ && !record.components && !record.children.length) {
      warn(
        `Record with path "${record.path}" is either missing a "component(s)"` +
          ` or "children" property.`
      )
    }

    for (const name in record.components) {
      let rawComponent = record.components[name]

      // 对组件类型合法性判断,不为object或function类型则直接退出
      if (__DEV__) {
        if (
          !rawComponent ||
          (typeof rawComponent !== 'object' &&
            typeof rawComponent !== 'function')
        ) {
          warn(
            `Component "${name}" in record with path "${record.path}" is not` +
              ` a valid component. Received "${String(rawComponent)}".`
          )
          // throw to ensure we stop here but warn to ensure the message isn't
          // missed by the user
          throw new Error('Invalid route component')
        }
        // 假如通过 import('./component.vue') 方式引入的,会自动转换成Promise函数组件
        else if ('then' in rawComponent) {
          // warn if user wrote import('/component.vue') instead of () =>
          // import('./component.vue')
          warn(
            `Component "${name}" in record with path "${record.path}" is a ` +
              `Promise instead of a function that returns a Promise. Did you ` +
              `write "import('./MyPage.vue')" instead of ` +
              `"() => import('./MyPage.vue')" ? This will break in ` +
              `production if not fixed.`
          )
          const promise = rawComponent
          rawComponent = () => promise
        }
        // 控制台提示 defineAsyncComponent()方式定义的组件
        else if (
          (rawComponent as any).__asyncLoader &&
          // warn only once per component
          !(rawComponent as any).__warnedDefineAsync
        ) {
          ;(rawComponent as any).__warnedDefineAsync = true
          warn(
            `Component "${name}" in record with path "${record.path}" is defined ` +
              `using "defineAsyncComponent()". ` +
              `Write "() => import('./MyPage.vue')" instead of ` +
              `"defineAsyncComponent(() => import('./MyPage.vue'))".`
          )
        }
      }

      // 当遇到`beforeRouteUpdate`或`beforeRouteLeave`钩子时,检查组件没有挂载则终止
      // record.instances会在组件mounted周期进行赋值
      if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue

      // 再次判断组件类型,这里同时支持 class component方式构建的组件
      if (isRouteComponent(rawComponent)) {
        // __vccOpts is added by vue-class-component and contain the regular options
        const options: ComponentOptions =
          (rawComponent as any).__vccOpts || rawComponent
        const guard = options[guardType]

        // 构建守卫的Promise链
        guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
      }
      // 对于异步加载的组件的守卫提取,这里同时支持异步 class component方式构建的组件
      else {
        // start requesting the chunk already
        let componentPromise: Promise<
          RouteComponent | null | undefined | void
        > = (rawComponent as Lazy<RouteComponent>)()

        // 当使用函数组件并且没有返回Promise时,要提醒必须在组件里添加displayName,否则会在构建后运行出错
        if (__DEV__ && !('catch' in componentPromise)) {
          warn(
            `Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`
          )
          // 开发环境下,用Promise做封装处理
          componentPromise = Promise.resolve(componentPromise as RouteComponent)
        }

        // 提取组件内导航守卫到guards数组
        guards.push(() =>
          componentPromise.then(resolved => {
            if (!resolved)
              return Promise.reject(
                new Error(
                  `Couldn't resolve component "${name}" at "${record.path}"`
                )
              )
            const resolvedComponent = isESModule(resolved)
              ? resolved.default
              : resolved
            // replace the function with the resolved component
            // cannot be null or undefined because we went into the for loop
            record.components![name] = resolvedComponent
            // 兼容class component
            const options: ComponentOptions =
              (resolvedComponent as any).__vccOpts || resolvedComponent
            const guard = options[guardType]

            // 构建守卫的Promise链并返回
            return guard && guardToPromiseFn(guard, to, from, record, name)()
          })
        )
      }
    }
  }

  return guards
}

在完成守卫提取后,接下来就是执行里面逻辑了。

beforeRouteUpdate

源码:

.then(() => {
  // check in components 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)

  // run the queue of per route beforeEnter guards
  return runGuardQueue(guards)
})

beforeRouteEnter

源码:

.then(() => {
  // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

  // clear existing enterCallbacks, these are added by extractComponentsGuards
  to.matched.forEach(record => (record.enterCallbacks = {}))

  // check in-component beforeRouteEnter
  guards = extractComponentsGuards(
    enteringRecords,
    'beforeRouteEnter',
    to,
    from
  )
  guards.push(canceledNavigationCheck)

  // run the queue of per route beforeEnter guards
  return runGuardQueue(guards)
})

beforeRouteLeave

源码:

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {
  let guards: Lazy<any>[]

  const [leavingRecords, updatingRecords, enteringRecords] =
    extractChangingRecords(to, from)

    // `navigate`方法会在路由跳转时执行,因此`beforeRouteLeave`守卫得优先执行。下面的`guards`数组会保存`beforeRouteLeave`守卫回调逻辑
    guards = extractComponentsGuards(
      leavingRecords.reverse(),
      'beforeRouteLeave',
      to,
      from
    )

    // leavingRecords is already reversed
    for (const record of leavingRecords) {
      record.leaveGuards.forEach(guard => {
        guards.push(guardToPromiseFn(guard, to, from))
      })
    }

    const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
      null,
      to,
      from
    )

    guards.push(canceledNavigationCheck)

    // run the queue of per route beforeRouteLeave guards
    return (
      // 执行`beforeRouteLeave`守卫
    runGuardQueue(guards)
    // ...
  )
}

落幕

到此,所有的导航守卫研读完毕,最后感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹