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 对象有三种方法:
- createWebHashHistory():创建一个 hash history,适用于不用 host 或没 server 配合的,但对 SEO 不友好
- createWebHistory():创建一个 HTML5 history,需要后端配置下 server
- 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()
install()3. 内部方法
这部分介绍几个重要的内部方法。
- 监听 popstate 事件进行前端视图的切换 - pushWithRedirect()重定向导航
- 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()
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()
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 = RouteRecordNormalizedLast updated