Skip to content

第 16 章 Vue Router 内核

本章要点

  • 路由的本质:URL 与组件树的映射关系
  • createRouter 的架构:matcher(路由匹配)+ history(URL 管理)+ 导航守卫的三位一体
  • 路由匹配器:如何将 /user/:id/posts 这样的路径模式编译为高效的正则表达式
  • History 模式的实现差异:createWebHistory vs createWebHashHistory vs createMemoryHistory
  • 导航解析的完整流程:从 router.push 到组件渲染的 17 个步骤
  • 导航守卫的洋葱模型:beforeEach → beforeRouteUpdate → beforeEnter → beforeRouteEnter → afterEach
  • RouterView 的实现:如何利用 provide/inject 实现嵌套路由
  • 路由懒加载与代码分割的底层机制

URL 是 Web 应用的"灵魂"。用户分享链接、浏览器前进后退、SEO 爬虫抓取——所有这些都依赖于 URL 与应用状态的正确映射。Vue Router 就是管理这种映射关系的核心库。

表面上看,路由只是"URL 变了就渲染对应组件"。但深入内核你会发现,这背后涉及路径模式的编译与匹配、浏览器 History API 的封装、异步导航守卫的流程控制、嵌套路由的组件协调等一系列精密的工程实现。

16.1 整体架构

Vue Router 4 的核心由三个模块组成:

createRouter 的入口

typescript
export function createRouter(options: RouterOptions): Router {
  // 1. 创建匹配器
  const matcher = createRouterMatcher(options.routes, options)

  // 2. 获取 history 实现
  const routerHistory = options.history

  // 3. 导航守卫数组
  const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
  const afterGuards = useCallbacks<NavigationHookAfter>()

  // 4. 当前路由(响应式)
  const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
    START_LOCATION_NORMALIZED
  )

  const router: Router = {
    currentRoute,
    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: errorListeners.add,
    isReady,
    install(app: App) { /* ... */ },
  }

  return router
}

注意 currentRoute 使用的是 shallowRef 而不是 ref。这是因为路由对象包含大量嵌套属性(params、query、matched 等),深层响应式会带来不必要的性能开销。路由变化时,整个对象被替换(而不是修改内部属性),所以浅层响应足矣。

16.2 路由匹配器

路径模式的编译

当你定义 /user/:id/posts 这样的路径时,Vue Router 需要将它编译为能匹配实际 URL 的正则表达式。这个过程分两步:

第一步:词法分析(tokenizePath)

typescript
// 将路径字符串分解为 token 数组
tokenizePath('/user/:id/posts')
// 输出:
[
  [{ type: TokenType.Static, value: 'user' }],
  [{ type: TokenType.Param, value: 'id', regexp: '', repeat: false, optional: false }],
  [{ type: TokenType.Static, value: 'posts' }]
]

每个路径段被分解为一个 token 数组。token 类型包括:

类型示例说明
Staticuser固定字符串
Param:id动态参数
Param + regexp:id(\\d+)带约束的参数
Param + repeat:chapters+可重复参数
Param + optional:lang?可选参数

第二步:编译为正则(tokensToParser)

typescript
function tokensToParser(
  segments: Array<Token[]>,
  extraOptions?: PathParserOptions
): PathParser {
  let score: Array<number[]> = []
  let pattern = options.start ? '^' : ''
  const keys: PathParserParamKey[] = []

  for (const segment of segments) {
    const segmentScores: number[] = []
    pattern += '/'

    for (const token of segment) {
      if (token.type === TokenType.Static) {
        pattern += token.value.replace(REGEX_CHARS_RE, '\\$&')
        segmentScores.push(PathScore.Static)
      } else {
        // 参数
        keys.push(token)
        const re = token.regexp ? token.regexp : BASE_PARAM_PATTERN
        pattern += token.repeat
          ? `((?:${re})(?:/(?:${re}))*)`
          : `(${re})`
        segmentScores.push(
          token.regexp ? PathScore.BonusCustomRegExp : PathScore.Dynamic
        )
      }
    }

    score.push(segmentScores)
  }

  const re = new RegExp(pattern, options.sensitive ? '' : 'i')

  // 返回 parser 对象
  return {
    re,
    score,
    keys,
    parse(path) { /* 用 re 匹配并提取参数 */ },
    stringify(params) { /* 将参数填入模式生成路径 */ },
  }
}

路由评分系统

当多条路由都能匹配同一个 URL 时,Vue Router 使用评分系统选择最佳匹配:

typescript
enum PathScore {
  _multiplier = 10,
  Root = 9 * _multiplier,           // /
  Segment = 4 * _multiplier,        // /segment
  SubSegment = 3 * _multiplier,     // /multiple-:things
  Static = 4 * _multiplier,         // /static
  Dynamic = 2 * _multiplier,        // /:param
  BonusCustomRegExp = 1 * _multiplier, // /:id(\\d+)
  BonusWildcard = -4 * _multiplier - PathScore.BonusCustomRegExp, // /:path(.*)
  BonusOptional = -0.8 * _multiplier, // /:id?
  BonusStrict = 0.07 * _multiplier, // 严格模式
  BonusCaseSensitive = 0.025 * _multiplier, // 大小写敏感
}

规则直觉:

  • 静态路径 > 动态路径/user/profile 优先于 /user/:id
  • 有约束 > 无约束/user/:id(\\d+) 优先于 /user/:id
  • 必选 > 可选/user/:id 优先于 /user/:id?
  • 通配符最低/:path(.*) 只在没有其他匹配时才命中

createRouterMatcher

typescript
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher {
  const matchers: RouteRecordMatcher[] = []
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()

  function addRoute(record: RouteRecordRaw, parent?: RouteRecordMatcher) {
    const normalizedRecord = normalizeRouteRecord(record)

    // 处理嵌套路由
    if (parent) {
      normalizedRecord.path = parent.record.path + '/' + normalizedRecord.path
    }

    const matcher: RouteRecordMatcher = createRouteRecordMatcher(
      normalizedRecord,
      parent,
      globalOptions
    )

    // 按 score 排序插入
    insertMatcher(matcher)

    // 递归处理子路由
    if ('children' in normalizedRecord && normalizedRecord.children) {
      for (const child of normalizedRecord.children) {
        addRoute(child, matcher)
      }
    }
  }

  function resolve(location: MatcherLocationRaw): MatcherLocation {
    if (location.name) {
      // 命名路由:直接通过 Map 查找
      const matcher = matcherMap.get(location.name)
      // ...
    } else if (location.path) {
      // 路径匹配:遍历 matchers 数组,按 score 排序已保证优先匹配高分路由
      for (const matcher of matchers) {
        const parsed = matcher.re.exec(location.path)
        if (parsed) {
          // 提取参数,构造路由位置
          return /* ... */
        }
      }
    }
  }

  // 初始化:添加所有路由
  routes.forEach(route => addRoute(route))

  return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}

命名路由通过 Map 实现 O(1) 查找;路径路由通过排序后的数组实现"优先匹配高分"。

16.3 History 模式

统一接口

typescript
interface RouterHistory {
  readonly base: string
  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
  createHref(location: HistoryLocation): string
  destroy(): void
}

三种 History 实现共享相同的接口,Router 不关心底层是 HTML5 History API、Hash 还是内存模式。

createWebHistory

typescript
export function createWebHistory(base?: string): RouterHistory {
  base = normalizeBase(base)

  const historyNavigation = useHistoryStateNavigation(base)
  const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location,
    historyNavigation.replace
  )

  function go(delta: number, triggerListeners = true) {
    if (!triggerListeners) historyListeners.pauseListeners()
    history.go(delta)
  }

  const routerHistory: RouterHistory = Object.assign(
    { location: '', base, go },
    historyNavigation,
    historyListeners
  )

  // 拦截 popstate 事件
  Object.defineProperty(routerHistory, 'location', {
    enumerable: true,
    get: () => historyNavigation.location.value,
  })

  return routerHistory
}

基于 VitePress 构建