Appearance
第 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 类型包括:
| 类型 | 示例 | 说明 |
|---|---|---|
| Static | user | 固定字符串 |
| 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
}