3. createRouter()

该方法接收一个 RouterOptions 参数,返回一个 Router 实例。

简化后的代码如下:

// 文件 router.ts
export function createRouter(options: RouterOptions): Router {
  ...
  const router: Router = {
    currentRoute,
    listening: true,

    addRoute,
    removeRoute,
    hasRoute,
    getRoutes,
    resolve,
    options,

    push,
    replace,
    go,
    back: () => go(-1),
    forward: () => go(1),

    beforeEach: beforeGuards.add,
    beforeResolve: beforeResolveGuards.add,
    afterEach: afterGuards.add,

    onError: errorHandlers.add,
    isReady,

    install(app: App) {
      ...
    },
  }
  
  return router
}

1. 输入 RouterOptions

1.1 接口定义

RouterOptions 的接口定义如下:

// 文件 router.ts
export interface RouterOptions extends PathParserOptions {
  history: RouterHistory             // 路由的 History 实现
  routes: Readonly<RouteRecordRaw[]> // 路由列表
  scrollBehavior?: RouterScrollBehavior  // 路由切换时控制页面的滚动
  parseQuery?: typeof originalParseQuery          // 自定义 query 解析
  stringifyQuery?: typeof originalStringifyQuery  // 自定义 stringify query 对象
  linkActiveClass?: string       // 默认是 router-link-active
  linkExactActiveClass?: string  // 默认是 router-link-exact-active
}

1.2 history 选项

RouterHistory 接口的定义如下:

// 文件 history/common.ts
export interface RouterHistory {
  readonly base: string              // 附加到每个 url 前的 base path
  readonly location: HistoryLocation // 当前路由
  readonly state: HistoryState       // 当前路由的状态
  push(to: HistoryLocation, data?: HistoryState): void
  replace(to: HistoryLocation, data?: HistoryState): void
  go(delta: number, triggerListeners?: boolean): void
  listen(callback: NavigationCallback): () => void // 路由事件监听器,当导航从 outside 触发时(比如浏览器的前进和后退按钮)
  destroy(): void                                  // 清除路由上所有事件的 listeners
  createHref(location: HistoryLocation): string    // 生成在 anchor tag 里使用的 href
}

创建 RouterHistory 对象有三种方法:

  1. createWebHashHistory():创建一个 hash history,适用于不用 host 或没 server 配合的,但对 SEO 不友好

  2. createWebHistory():创建一个 HTML5 history,需要后端配置下 server

  3. createMemoryHistory():创建一个基于内存的 history,主要用来处理 SSR,不运行在浏览器端

server {
  ...
  location / { 
    try_files $uri $uri/ /index.html; # 前端路由的统一入口
  }
}

除了对 base 处理的逻辑不同之外,hash 模式和 HTML5 模式的其它逻辑是一样的,因为 Vue Router 4(即 Vue3)对应的浏览器都支持 HTML5 Web History API。

// 文件 src/history/hash.ts
export function createWebHashHistory(base?: string): RouterHistory {
  base = location.host ? base || location.pathname + location.search : ''
  if (!base.includes('#')) base += '#'

  if (__DEV__ && !base.endsWith('#/') && !base.endsWith('#')) {
    warn(
      `A hash base must end with a "#":\n"${base}" should be "${base.replace(
        /#.*$/,
        '#'
      )}".`
    )
  }
  return createWebHistory(base)
}

对于 createWebHistory() 的介绍,详见 createWebHistory()

1.3 routes 选项

createRouter() 方法中,只有一个地方用到了 options.routes

// 文件 router.ts
export function createRouter(options: RouterOptions): Router {
  // matcher 模块,创建一个 Router Matcher
  const matcher = createRouterMatcher(options.routes, options)
}

对于 createRouterMatcher() 方法的介绍,详见 createRouterMatcher()

2. 输出 Router 实例

2.1 接口定义

Router 实例的接口定义如下:

// Router instance
export interface Router {
  // 只读属性
  readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
  readonly options: RouterOptions  // 传给 createRouter() 的原始 options 对象

  // 是否关闭对 history 事件的监听,是个底层 API
  listening: boolean               

  // 对 route records 的增删查
  addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void  // 给已经存在的 route 添加一个新的子 route record
  addRoute(route: RouteRecordRaw): () => void                               // 给 router 添加一个新的 route record
  removeRoute(name: RouteRecordName): void  // 移除
  hasRoute(name: RouteRecordName): boolean  // 检查是否存在
  getRoutes(): RouteRecord[]                // 获取所有 route records 的完整列表

  // 返回 route location 的 RouteLocation(标准化版本)
  resolve(
    to: RouteLocationRaw,
    currentLocation?: RouteLocationNormalizedLoaded
  ): RouteLocation & { href: string }

  // 通过操作 history 栈,用编程的方式导航到新 URL
  push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
  replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
  
  // 在 history 中前进或后退
  go(delta: number): void
  back(): ReturnType<Router['go']>     // 等价于 `router.go(-1)`
  forward(): ReturnType<Router['go']>  // 等价于 `router.go(1)`

  // 导航钩子
  beforeEach(guard: NavigationGuardWithThis<undefined>): () => void    // 在每次导航之前执行
  beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void // 在导航即将被 resolved 之前执行,此时所有组件已成功拉取,导航守卫也已成功
  afterEach(guard: NavigationHookAfter): () => void                    // 在每次导航之后执行

  // 错误处理程序,会在导航期间发生未捕获 error 的时候触发
  // 包括同步错误和异步错误、在导航守卫中传给 `next`的错误、渲染路由所需异步组件时的错误
  onError(handler: _ErrorHandler): () => void
  
  // 当 router 完成了初始导航就会 resolve 该 Promise,包括和初始路由相关的所有异步钩子和异步组件
  // 在服务器端渲染时会很有用,因为此时需要是开发人员手动 push 初始 location(客户端时 router 会自动从 URL 中获取到)
  isReady(): Promise<void>

  // 当执行 `app.use(router)` 时会被自动调用
  // 会在客户端触发初始导航
  install(app: App): void  
}

2.2 install()

3. 内部方法

这部分介绍几个重要的内部方法。

  1. 监听 popstate 事件进行前端视图的切换

    1. pushWithRedirect() 重定向导航

    2. navigate() 普通导航

// 文件 router.ts
export function createRouter(options: RouterOptions): Router {

  function pushWithRedirect(
    to: RouteLocationRaw | RouteLocation,
    redirectedFrom?: RouteLocation
  ): Promise<NavigationFailure | void | undefined> { }

  function navigate(
    to: RouteLocationNormalized,
    from: RouteLocationNormalizedLoaded
  ): Promise<any> { }

  return router
}

3.1 pushWithRedirect()

简化后的代码,如下:

function pushWithRedirect(
  to: RouteLocationRaw | RouteLocation,
  redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {

  const targetLocation: RouteLocation = (pendingLocation = resolve(to))
  const from = currentRoute.value

  const shouldRedirect = handleRedirectRecord(targetLocation)

  // 如果需要重定向,则递归调用
  if (shouldRedirect)
    return pushWithRedirect(...)

  // 否则调用 navigate()
  const toLocation = targetLocation as RouteLocationNormalized
  toLocation.redirectedFrom = redirectedFrom

  return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
    .catch(...)
    .then(...)
}

3.2 navigate()

源码如下:(没有看到明显的组件切换 ???)

function navigate(
  to: RouteLocationNormalized,
  from: RouteLocationNormalizedLoaded
): Promise<any> {

  // 1. 提取路由记录 RouteRecord
  const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from)

  // 2. guards.push()
  let guards: Lazy<any>[]

  // 这里所有的组件都已经被 resolved 了一次,因为我们 are leaving
  guards = extractComponentsGuards(
    leavingRecords.reverse(), // 原地操作,反转
    'beforeRouteLeave',
    to,
    from
  )

  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)

  // 3. 运行每个 route 的 beforeRouteLeave 守卫队列
  return (
    runGuardQueue(guards)
      .then(() => {
        // 检查全局守卫, beforeEach
        guards = []
        for (const guard of beforeGuards.list()) {
          guards.push(guardToPromiseFn(guard, to, from))
        }
        guards.push(canceledNavigationCheck)

        return runGuardQueue(guards)
      })
      .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)

        // 运行每个 route 的 beforeEnter 守卫队列
        return runGuardQueue(guards)
      })
      .then(() => {
        // 检查 route 的 beforeEnter
        guards = []
        for (const record of to.matched) {
          // 对于 reused views,不触发 beforeEnter
          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)

        // 运行每个 route 的 beforeEnter 守卫队列
        return runGuardQueue(guards)
      })
      .then(() => {
        // 注意:此时 to.matched 已被规范化,且不包含任何 () => Promise<Component>

        // 清除现有的 enterCallbacks, 它们是由 extractComponentsGuards 添加的
        to.matched.forEach(record => (record.enterCallbacks = {}))

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

        // 运行每个 route 的 beforeEnter 守卫队列
        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 所有取消的any navigation
      .catch(err =>
        isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
          ? err
          : Promise.reject(err)
      )
  )
}


function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
  return guards.reduce(
    (promise, guard) => promise.then(() => guard()),
    Promise.resolve()
  )
}

相关 types 定义如下:

// 文件 matcher/types.ts
export interface RouteRecordNormalized {

  path: _RouteRecordBase['path']
  redirect: _RouteRecordBase['redirect'] | undefined
  name: _RouteRecordBase['name']
  components: RouteRecordMultipleViews['components'] | null | undefined
  children: RouteRecordRaw[]  // 嵌套的 route records
  
  meta: Exclude<_RouteRecordBase['meta'], void>
  props: Record<string, _RouteRecordProps>

  beforeEnter: _RouteRecordBase['beforeEnter']  // 注册 beforeEnter guards
  leaveGuards: Set<NavigationGuard>  // 注册 leave guards
  updateGuards: Set<NavigationGuard>  // 注册 update guards

  enterCallbacks: Record<string, NavigationGuardNextCallback[]>  // 注册 beforeRouteEnter 回调
  
  instances: Record<string, ComponentPublicInstance | undefined | null>  // 挂载的 route component 实例

  aliasOf: RouteRecordNormalized | undefined
}

export type RouteRecord = RouteRecordNormalized

Last updated