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) { /* 将参数填入模式生成路径 */ },
  }
}

基于 VitePress 构建