Skip to content

第 8.1 节:Vue Router - 路由匹配器(Matcher)的原理

概述

Vue Router 是一个“状态机”,它负责将“URL”映射到“组件树”。

这个过程由两个独立的系统协同完成:

  1. RouterHistory(历史管理器):由 createWebHistorycreateWebHashHistory 创建。它的职责是**“监听”listen)浏览器的地址栏变化,并通过 pushreplace 等方法“修改”**地址栏。
  2. RouterMatcher(匹配器):由 createRouterMatcher 创建。它的职责是维护一个“路由规则列表”,并提供一个 resolve 方法,用于**“查询”**一个 URL 路径应该匹配哪些路由规则。

本节将深入分析 RouterMatcher 是如何构建这个“规则列表”并实现高效查询的。


1. createRouterMatcher:路由规则的管理者

createRouterMatcher (packages/router/src/matcher/index.ts) 接收用户定义的 routes 数组,并返回一个 RouterMatcher 对象。这个对象的核心就是 addRouteresolve 方法。

typescript
// packages/router/src/matcher/index.ts
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>,
  globalOptions: PathParserOptions
): RouterMatcher {
  // 1. matchers: 一个数组,存储所有“路由解析器”
  //    (按“匹配优先级”排序)
  const matchers: RouteRecordMatcher[] = []
  
  // 2. matcherMap: 一个 Map,用于通过“name”快速查找
  const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()

  // 【写入数据】
  function addRoute(record: RouteRecordRaw, parent?: RouteRecordMatcher) {
    // ...
  }
  
  // 【查询数据】
  function resolve(
    location: MatcherLocationRaw, // { path: '/user/123' }
    currentLocation: MatcherLocation
  ): MatcherLocation {
    // ...
  }

  // 【删除数据】
  function removeRoute(name: RouteRecordName) {
    // ...
  }
  
  // 初始化:将用户传入的 routes 注册到 matchers 数组中
  routes.forEach(route => addRoute(route))

  return { addRoute, resolve, removeRoute, ... }
}

2. addRoute:“写入”路由规则

addRoute 负责将一个用户定义的路由对象(RouteRecordRaw)转换为一个“路由解析器”(RouteRecordMatcher),并将其按顺序插入到 matchers 数组中。

这个过程分为三步:规范化、标记化、生成解析器。

第 1 步:规范化 (normalizeRouteRecord)

addRoute 首先调用 normalizeRouteRecord,将用户的写法统一成 RouteRecordNormalized 格式(例如,component 统一为对象,处理 alias 别名等)。

第 2 步:标记化 (tokenizePath)

tokenizePath (pathTokenizer.ts) 负责将“路径字符串”“标记化”(tokenize)为一个“词法单元”数组。

  • 输入/user/:id(\\d+)?/profile
  • tokenizePath 输出 (概念):
    javascript
    [
      // segment 1
      [ { type: 'Static', value: 'user' } ],
      // segment 2
      [ 
        { type: 'Param', value: 'id', 
          regexp: '\\d+', optional: true, repeatable: false } 
      ],
      // segment 3
      [ { type: 'Static', value: 'profile' } ]
    ]

第 3 步:生成解析器 (tokensToParsercreateRouteRecordMatcher)

createRouteRecordMatcher 接收“规范化”后的路由,并调用 tokensToParser,将“词法单元”数组**“编译”成一个“路径解析器”** (PathParser)。

typescript
// packages/router/src/matcher/pathMatcher.ts
export function createRouteRecordMatcher(
  record: Readonly<RouteRecordNormalized>,
  parent: RouteRecordMatcher | undefined,
  options?: PathParserOptions
): RouteRecordMatcher {
  
  // 1. “标记化”
  const tokens = tokenizePath(record.path)
  
  // 2. 【编译】
  //    将“词法单元”编译成一个“解析器”
  const parser: PathParser = tokensToParser(tokens, options)

  // 3. 创建 Matcher,它包含了“解析器”和“原始路由记录”
  const matcher: RouteRecordMatcher = {
    ...parser, // 包含 .re, .score, .keys, .parse()
    record,
    parent,
    children: [],
    alias: [],
  }

  // 4. 处理嵌套路由 (递归调用 addRoute)
  if (record.children) {
    for (const child of record.children) {
      addRoute(child, matcher)
    }
  }

  // 5. 【入库】
  //    将这个 matcher 按“评分”插入 matchers 数组
  insertMatcher(matcher)
  
  return matcher
}

3. tokensToParser:路径解析的核心

tokensToParser (pathParserRanker.ts) 是 Vue Router 匹配系统的核心。它根据“词法单元”数组,生成一个包含正则表达式匹配评分的对象。

typescript
// packages/router/src/matcher/pathParserRanker.ts
export function tokensToParser(
  segments: Array<Token[]>, // “词法单元”
  // ...
): PathParser {
  
  let pattern = '' // 用于构建正则表达式
  const keys: PathParserParamKey[] = [] // 存储参数名
  const score: Array<number[]> = [] // 存储评分

  // 遍历所有“段”(segments)
  for (const segment of segments) {
    const segmentScores: number[] = []
    
    // 遍历每段中的“词法单元”
    for (const token of segment) {
      if (token.type === TokenType.Static) {
        // 1. 【静态】
        pattern += '/' + token.value // e.g., /user
        segmentScores.push(PathScore.Static)
        
      } else if (token.type === TokenType.Param) {
        // 2. 【动态】
        const { value, regexp, optional, repeatable } = token
        keys.push({ name: value, ... })
        
        // e.g., /(\\d+)?  或 /([^/]+?)
        const re = regexp ? `(${regexp})` : `([^/]+?)` 
        pattern += optional ? `(?:/${re})?` : `/${re}`
        
        // 评分:动态 < 静态
        segmentScores.push(PathScore.Dynamic) 
        if (optional) segmentScores.push(PathScore.BonusOptional)
        // ...
      }
    }
    score.push(segmentScores)
  }

  // 3. 【编译正则】
  const re = new RegExp('^' + pattern + '$', 'i') // 'i' = 不区分大小写
  
  // 4. 【解析 Params 的方法】
  function parse(path: string): PathParams | null {
    const match = path.match(re)
    if (!match) return null
    
    const params: PathParams = {}
    // 从正则的“捕获组”中提取 params
    for (let i = 1; i < match.length; i++) {
      const value = match[i] || ''
      const key = keys[i - 1]
      params[key.name] = value
    }
    return params
  }

  return { re, score, keys, parse, ... }
}

score (评分) 的作用: 评分系统(PathScore)保证了**“更具体”的路由会优先**被匹配。

  • /user/foo (静态) 的得分 > /user/:id (动态)
  • /user/:id(\\d+) (带正则) 的得分 > /user/:id (不带正则)

addRouteinsertMatcher 时,会根据 score 进行排序,确保 matchers 数组中,“最具体”的规则排在最前面


4. resolve:“查询”路由规则

resolve (packages/router/src/matcher/index.ts) 是 RouterHistory(历史管理器)唯一会调用的 matcher 方法。当 URL 变化时,History 会调用 resolve 来找出该匹配哪个组件。

typescript
// packages/router/src/matcher/index.ts
function resolve(
  location: MatcherLocationRaw, // { path: '/user/123/profile' }
  currentLocation: MatcherLocation
): MatcherLocation {
  
  let path = location.path
  // (省略 query, hash, 相对路径的处理)
  
  const matched: RouteLocationNormalized['matched'] = []
  let matcher: RouteRecordMatcher | undefined
  
  // 1. 【遍历规则】
  //    matchers 数组已按“评分”排序
  for (const m of matchers) {
    // 2. 【执行正则】
    const params = m.parse(path) // m.parse() 是 tokensToParser 生成的
    
    if (params) {
      // 3. 【匹配成功!】
      matcher = m
      
      // 4. 【构建匹配链】
      //    从当前匹配项,一路回溯到根路由
      //    e.g., [ RootRoute, UserRoute, UserProfileRoute ]
      let parent = matcher
      while (parent) {
        matched.unshift(parent.record)
        parent = parent.parent
      }

      // 5. 【返回结果】
      return {
        path,
        params,
        matched, // 匹配的“面包屑”数组
        // ...
      }
    }
  }

  // 6. 【匹配失败】
  //    (返回 404 Not Found 的逻辑)
}

总结

Vue Router 的 matcher 是一个管理“路由规则”的系统:

  1. “写入” (addRoute)
    • tokenizePath (标记化):/user/:id -> ['user', { Param 'id' }]
    • tokensToParser (编译):生成一个包含 RegExp (正则表达式)、score (评分) 和 parse (参数提取) 方法的**“解析器”**。
    • matchers.push (入库):按 score 有序插入matchers 数组。
  2. “查询” (resolve)
    • 遍历 matchers 数组,用 RegExp 逐个匹配 URL。
    • 由于 matchers 是有序的,第一个匹配成功的一定是“最佳匹配”
    • 返回一个包含 matched (路由记录) 和 params (参数) 的 MatcherLocation 对象。

RouterHistory(下节内容)负责“监听 URL 变化”,一旦变化,就调用 matcher.resolve(),拿到“查询结果”,然后触发导航守卫和组件更新。


微信公众号二维码