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