Appearance
第 8.1 节:Vue Router - 路由匹配器(Matcher)的原理
概述
Vue Router 是一个“状态机”,它负责将“URL”映射到“组件树”。
这个过程由两个独立的系统协同完成:
RouterHistory(历史管理器):由createWebHistory或createWebHashHistory创建。它的职责是**“监听”(listen)浏览器的地址栏变化,并通过push、replace等方法“修改”**地址栏。RouterMatcher(匹配器):由createRouterMatcher创建。它的职责是维护一个“路由规则列表”,并提供一个resolve方法,用于**“查询”**一个 URL 路径应该匹配哪些路由规则。
本节将深入分析 RouterMatcher 是如何构建这个“规则列表”并实现高效查询的。
1. createRouterMatcher:路由规则的管理者
createRouterMatcher (packages/router/src/matcher/index.ts) 接收用户定义的 routes 数组,并返回一个 RouterMatcher 对象。这个对象的核心就是 addRoute 和 resolve 方法。
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 步:生成解析器 (tokensToParser 和 createRouteRecordMatcher)
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(不带正则)
addRoute 在 insertMatcher 时,会根据 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 是一个管理“路由规则”的系统:
- “写入” (
addRoute):tokenizePath(标记化):/user/:id->['user', { Param 'id' }]tokensToParser(编译):生成一个包含RegExp(正则表达式)、score(评分) 和parse(参数提取) 方法的**“解析器”**。matchers.push(入库):按score有序插入到matchers数组。
- “查询” (
resolve):- 遍历
matchers数组,用RegExp逐个匹配 URL。 - 由于
matchers是有序的,第一个匹配成功的一定是“最佳匹配”。 - 返回一个包含
matched(路由记录) 和params(参数) 的MatcherLocation对象。
- 遍历
RouterHistory(下节内容)负责“监听 URL 变化”,一旦变化,就调用 matcher.resolve(),拿到“查询结果”,然后触发导航守卫和组件更新。
