基于源码深度解析
2026-03-17 | 技术深度解读
第一部分:基础架构
第二部分:演进历史
第三部分:核心类解析
第四部分:核心函数
Vue Router 是 Vue.js 官方的路由管理器,为单页应用提供导航控制。
核心功能
技术特点
┌─────────────────────────────────────────────────────────┐
│ Vue Router │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Router │ │ History │ │ Guards │ │
│ │ Core │ │ Manager │ │ System │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴────────────────┴──────┐ │
│ │ RouterMatcher │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Tokenizer│ │ Parser │ │ Ranker │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
路由配置 → 分词器 → 解析器 → 评分排序 → 匹配器数组
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ /users/ │ │ Token[] │ │PathParser│ │ Matcher │
│ :id │ -> │ Static │ -> │ re │ -> │ sorted │
│ │ │ Param │ │ score │ │ by │
│ │ │ │ │ keys │ │ score │
└─────────┘ └─────────┘ └─────────┘ └─────────┘
URL 输入 → 正则匹配 → 参数解析 → 路由记录
关键步骤:路径字符串经过分词、解析、评分后生成有序的匹配器
| 层级 | 模块 | 职责 |
|---|---|---|
| 应用层 | Router | 对外接口、导航控制 |
| 匹配层 | RouterMatcher | 路由注册、URL 解析 |
| 解析层 | PathParser | 正则生成、参数提取 |
| 分词层 | Tokenizer | 路径字符串分词 |
| 历史层 | RouterHistory | History API 封装 |
Vue Router 3.x
Vue Router 4.x
重大变化:移除了 * 通配符,改用参数自定义正则
// 动态路由参数正则
const routes = [
{ path: '/user/:id(\\d+)' }, // 数字 ID
{ path: '/:path(.*)*', } // 捕获所有
{ path: '/:orderId(\\d+)', }, // 重复参数
{ path: '/:chapters(\\d+)+' }, // 重复参数
]
// 可选参数
{ path: '/user/:id?' } // 可选
{ path: '/user/:id*' } // 0 或多个
{ path: '/user/:id+' } // 1 或多个
Vue Router 路由匹配系统由以下核心数据结构组成:
| 结构 | 文件 | 作用 |
|---|---|---|
| RouterMatcher | matcher/index.ts | 路由匹配器管理 |
| RouteRecordMatcher | matcher/pathMatcher.ts | 单条路由匹配器 |
| PathParser | matcher/pathParserRanker.ts | 路径解析器 |
| Token | matcher/pathTokenizer.ts | 路径分词单元 |
interface RouterMatcher {
// 添加路由(支持嵌套)
addRoute: (
record: RouteRecordRaw,
parent?: RouteRecordMatcher
) => () => void
// 移除路由
removeRoute(matcher: RouteRecordMatcher): void
removeRoute(name: RouteRecordNameGeneric): void
// 清空所有路由
clearRoutes: () => void
// 获取所有匹配器
getRoutes: () => RouteRecordMatcher[]
// 按名称获取
getRecordMatcher: (
name: RouteRecordNameGeneric
) => RouteRecordMatcher | undefined
// 解析位置(核心方法)
resolve: (
location: MatcherLocationRaw,
currentLocation: MatcherLocation
) => MatcherLocation
}
interface RouteRecordMatcher extends PathParser {
// 路由记录(规范化后)
record: RouteRecord
// 父级匹配器
parent: RouteRecordMatcher | undefined
// 子匹配器数组
children: RouteRecordMatcher[]
// 别名匹配器(需一起删除)
alias: RouteRecordMatcher[]
}
// 继承自 PathParser
// - re: RegExp 正则表达式
// - score: number[] 评分数组
// - keys: ParamKey[] 参数键列表
// - parse() 解析 URL
// - stringify() 生成 URL
设计:继承 PathParser,扩展树形结构和别名支持
interface PathParser {
// 用于匹配 URL 的正则
re: RegExp
// 评分数组(用于排序)
score: Array
// 路径中出现的参数键
keys: PathParserParamKey[]
// 解析 URL,返回参数对象
// 无匹配返回 null
parse(path: string): PathParams | null
// 将参数对象转为 URL 字符串
stringify(params: PathParams): string
}
核心:parse 和 stringify 实现双向转换
interface PathParserParamKey {
// 参数名称
name: string
// 是否可重复(+ 或 * 修饰符)
repeatable: boolean
// 是否可选(? 或 * 修饰符)
optional: boolean
}
// 示例
// :id → { name: 'id', repeatable: false, optional: false }
// :id? → { name: 'id', repeatable: false, optional: true }
// :id* → { name: 'id', repeatable: true, optional: true }
// :id+ → { name: 'id', repeatable: true, optional: false }
const enum TokenType {
Static, // 静态文本:/users/profile
Param, // 动态参数:/:id
Group, // 分组(未来扩展)
}
// 静态 Token
interface TokenStatic {
type: TokenType.Static
value: string
}
// 参数 Token
interface TokenParam {
type: TokenType.Param
value: string // 参数名
regexp?: string // 自定义正则
repeatable: boolean // + 或 *
optional: boolean // ? 或 *
}
interface RouteRecordNormalized {
path: string
redirect?: RouteRecordRedirectOption
name?: RouteRecordName
meta: RouteMeta
aliasOf?: RouteRecordNormalized
// 组件配置
components: Record | null
// Props 配置
props: Record
// 子路由
children: RouteRecordRaw[]
// 实例引用
instances: Record
// 导航守卫
leaveGuards: Set
updateGuards: Set
enterCallbacks: Record
beforeEnter?: NavigationGuard | NavigationGuard[]
}
interface RouterHistory {
// 当前位置(响应式)
location: HistoryLocation
// 状态对象(响应式)
state: HistoryState
// 基础路径
base: string
// 导航方法
push(to: HistoryLocation, data?: HistoryState): void
replace(to: HistoryLocation, data?: HistoryState): void
go(delta: number, triggerListeners?: boolean): void
// 监听导航
listen(callback: NavigationCallback): () => void
// 创建 href
createHref(location: HistoryLocation): string
}
interface StateEntry extends HistoryState {
// 后退位置
back: HistoryLocation | null
// 当前位置
current: HistoryLocation
// 前进位置
forward: HistoryLocation | null
// 历史栈位置
position: number
// 是否为替换操作
replaced: boolean
// 滚动位置
scroll: _ScrollPositionNormalized | null | false
}
设计:完整的双向链表结构,支持前进后退导航
interface Router {
// 当前路由(响应式)
readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
// 原始配置
readonly options: RouterOptions
// 路由管理
addRoute(route: RouteRecordRaw): () => void
addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
removeRoute(name: RouteRecordName): void
hasRoute(name: RouteRecordName): boolean
getRoutes(): RouteRecord[]
// 导航方法
push(to: RouteLocationRaw): Promise<NavigationFailure | void>
replace(to: RouteLocationRaw): Promise<NavigationFailure | void>
go(delta: number): void
back(): void
forward(): void
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
// 有序匹配器数组
const matchers: RouteRecordMatcher[] = []
// 名称 → 匹配器映射
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
// 初始化时添加所有路由
routes.forEach(route => addRoute(route))
return {
addRoute,
resolve,
removeRoute,
clearRoutes,
getRoutes,
getRecordMatcher,
}
}
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// 1. 规范化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// 2. 处理别名
const normalizedRecords = [mainNormalizedRecord]
if ('alias' in record) {
const aliases = typeof record.alias === 'string'
? [record.alias]
: record.alias!
for (const alias of aliases) {
normalizedRecords.push(normalizeRouteRecord({
...mainNormalizedRecord,
path: alias,
}))
}
}
// 3. 创建匹配器并插入
for (const normalizedRecord of normalizedRecords) {
const matcher = createRouteRecordMatcher(
normalizedRecord, parent, options
)
if (isMatchable(matcher)) insertMatcher(matcher)
}
}
function resolve(
location: MatcherLocationRaw,
currentLocation: MatcherLocation
): MatcherLocation {
let matcher: RouteRecordMatcher | undefined
let params: PathParams = {}
let path: string
let name: string
// 1. 按名称解析
if ('name' in location && location.name) {
matcher = matcherMap.get(location.name)
name = matcher.record.name
params = assign(
pickParams(currentLocation.params, requiredKeys),
pickParams(location.params, matcher.keys.map(k => k.name))
)
path = matcher.stringify(params)
}
// 2. 按路径解析
else if (location.path != null) {
path = location.path
matcher = matchers.find(m => m.re.test(path))
if (matcher) {
params = matcher.parse(path)!
name = matcher.record.name
}
}
// 3. 构建 matched 数组(从根到叶)
const matched = []
let parent = matcher
while (parent) {
matched.unshift(parent.record)
parent = parent.parent
}
return { name, path, params, matched, meta: mergeMetaFields(matched) }
}
function insertMatcher(matcher: RouteRecordMatcher) {
// 使用二分查找确定插入位置
const index = findInsertionIndex(matcher, matchers)
matchers.splice(index, 0, matcher)
// 只将原始记录添加到名称映射
if (matcher.record.name && !isAliasRecord(matcher)) {
matcherMap.set(matcher.record.name, matcher)
}
}
// 二分查找插入位置
function findInsertionIndex(
matcher: RouteRecordMatcher,
matchers: RouteRecordMatcher[]
) {
let lower = 0, upper = matchers.length
while (lower !== upper) {
const mid = (lower + upper) >> 1
const sortOrder = comparePathParserScore(matcher, matchers[mid])
if (sortOrder < 0) upper = mid
else lower = mid + 1
}
return upper
}
export function tokenizePath(path: string): Array<Token[]> {
// 路径分词为数组
// "/users/:id/posts" → [
// [{ type: Static, value: "users" }],
// [{ type: Param, value: "id", ... }],
// [{ type: Static, value: "posts" }]
// ]
let state: TokenizerState = TokenizerState.Static
const tokens: Array<Token[]> = []
let segment: Token[] = []
let buffer: string = ''
let customRe: string = ''
// 状态机遍历每个字符
while (i < path.length) {
char = path[i++]
switch (state) {
case TokenizerState.Static:
if (char === '/') finalizeSegment()
else if (char === ':') { consumeBuffer(); state = Param }
else addCharToBuffer()
break
// ...
}
}
}
┌─────────┐ '/' ┌──────────┐
│ Static │ ────────> │ 分段完成 │
│ (默认) │ └──────────┘
└────┬────┘
│ ':'
▼
┌─────────┐ '(' ┌───────────┐
│ Param │ ────────> │ ParamRegExp│
└────┬────┘ └─────┬─────┘
│ │ ')'
│ *?+ ▼
│ ┌───────────────┐
└──────────────>│ParamRegExpEnd │
└───────────────┘
特殊字符处理:
- \ : 转义下一个字符
- ( ) : 自定义正则
- * ? + : 参数修饰符
export function tokensToParser(
segments: Array<Token[]>,
extraOptions?: _PathParserOptions
): PathParser {
const score: Array = []
let pattern = options.start ? '^' : ''
const keys: PathParserParamKey[] = []
for (const segment of segments) {
const segmentScores: number[] = []
for (const token of segment) {
if (token.type === TokenType.Static) {
// 静态部分:转义特殊字符
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&')
subSegmentScore += PathScore.Static
} else if (token.type === TokenType.Param) {
// 参数部分:构建捕获组
keys.push({ name, repeatable, optional })
const re = token.regexp || '[^/]+?'
let subPattern = repeatable
? `((?:${re})(?:/(?:${re}))*)`
: `(${re})`
pattern += '/' + subPattern + (optional ? '?' : '')
}
segmentScores.push(subSegmentScore)
}
score.push(segmentScores)
}
return { re: new RegExp(pattern), score, keys, parse, stringify }
}
const enum PathScore {
_multiplier = 10,
Root = 9 * _multiplier, // 根路径 /
Segment = 4 * _multiplier, // /a-segment
SubSegment = 3 * _multiplier, // /multiple-:things
Static = 4 * _multiplier, // /static (高优先级)
Dynamic = 2 * _multiplier, // /:id (低优先级)
BonusCustomRegExp = 1 * _multiplier, // /:id(\\d+)
BonusWildcard = -4 * _multiplier - BonusCustomRegExp,
BonusRepeatable = -2 * _multiplier, // /:w+ 或 /:w*
BonusOptional = -0.8 * _multiplier, // /:w?
BonusStrict = 0.07 * _multiplier,
BonusCaseSensitive = 0.025 * _multiplier,
}
规则:静态路径 > 动态参数,精确匹配 > 通配符
export function comparePathParserScore(
a: Pick<PathParser, 'score'>,
b: Pick<PathParser, 'score'>
): number {
let i = 0
const aScore = a.score
const bScore = b.score
while (i < aScore.length && i < bScore.length) {
const comp = compareScoreArray(aScore[i], bScore[i])
if (comp) return comp
i++
}
// 处理特殊情况的边界
if (Math.abs(bScore.length - aScore.length) === 1) {
if (isLastScoreNegative(aScore)) return 1
if (isLastScoreNegative(bScore)) return -1
}
// 分数相同时,更长的优先
return bScore.length - aScore.length
}
export function createRouter(options: RouterOptions): Router {
// 创建匹配器
const matcher = createRouterMatcher(options.routes, options)
// 历史管理
const routerHistory = options.history
// 守卫回调
const beforeGuards = useCallbacks<NavigationGuard>()
const beforeResolveGuards = useCallbacks<NavigationGuard>()
const afterGuards = useCallbacks<NavigationHookAfter>()
// 当前路由(响应式)
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
// 滚动恢复
if (isBrowser && options.scrollBehavior) {
history.scrollRestoration = 'manual'
}
return router
}
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
const targetLocation = pendingLocation = resolve(to)
const from = currentRoute.value
// 1. 处理重定向记录
const shouldRedirect = handleRedirectRecord(targetLocation, from)
if (shouldRedirect) {
return pushWithRedirect(
assign(locationAsObject(shouldRedirect), { force, replace }),
redirectedFrom || targetLocation
)
}
// 2. 检查重复导航
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
failure = createRouterError(ErrorTypes.NAVIGATION_DUPLICATED, { to, from })
}
// 3. 执行导航
return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
.then(failure => {
if (!failure) {
failure = finalizeNavigation(toLocation, from, true, replace, data)
}
triggerAfterEach(toLocation, from, failure)
return failure
})
}
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
const [leavingRecords, updatingRecords, enteringRecords] =
extractChangingRecords(to, from)
let guards: Lazy<any>[] = []
// 1. beforeRouteLeave (组件内)
guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from)
// 2. beforeEach (全局)
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
// 3. beforeRouteUpdate (组件内)
guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from)
// 4. beforeEnter (路由配置)
for (const record of enteringRecords) {
if (record.beforeEnter) guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
// 5. beforeRouteEnter (组件内)
guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from)
// 6. beforeResolve (全局)
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
return runGuardQueue(guards)
}
导航触发 → 目标路由
1. 触发离开的组件 beforeRouteLeave
2. 调用全局 beforeEach
3. 重用组件调用 beforeRouteUpdate
4. 路由配置的 beforeEnter
5. 解析异步路由组件
6. 进入的组件 beforeRouteEnter
7. 调用全局 beforeResolve
8. 导航确认
9. 调用全局 afterEach
10. DOM 更新后调用 beforeRouteEnter 的 next 回调
注意:beforeRouteEnter 的 next 回调在导航确认后执行
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 = assign(
{ location: '', base, go, createHref: createHref.bind(null, base) },
historyNavigation,
historyListeners
)
return routerHistory
}
function push(to: HistoryLocation, data?: HistoryState) {
// 更新当前条目的 forward 信息
const currentState = assign({}, historyState.value, {
forward: to,
scroll: computeScrollPosition(),
})
changeLocation(currentState.current, currentState, true) // replace
// 创建新条目
const state: StateEntry = assign(
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false) // push
currentLocation.value = to
}
function replace(to: HistoryLocation, data?: HistoryState) {
const state = assign({}, historyState.value, buildState(...), data)
changeLocation(to, state, true)
currentLocation.value = to
}
const popStateHandler = ({ state }: { state: StateEntry | null }) => {
const to = createCurrentLocation(base, location)
const from = currentLocation.value
const fromState = historyState.value
let delta = 0
if (state) {
currentLocation.value = to
historyState.value = state
delta = fromState ? state.position - fromState.position : 0
} else {
replace(to)
}
// 通知所有监听器
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta > 0 ? NavigationDirection.forward
: delta < 0 ? NavigationDirection.back
: NavigationDirection.unknown,
})
})
}
window.addEventListener('popstate', popStateHandler)
工厂模式
策略模式
观察者模式
组合模式
┌──────────────┐ ┌───────────────┐
│ Router │──────│ RouterMatcher │
└──────┬───────┘ └───────┬───────┘
│ │
│ uses │ contains
▼ ▼
┌──────────────┐ ┌───────────────────┐
│RouterHistory │ │RouteRecordMatcher │
└──────────────┘ └─────────┬─────────┘
│ extends
▼
┌─────────────────┐
│ PathParser │
│ - re: RegExp │
│ - score: [] │
│ - keys: [] │
└────────┬────────┘
│ created from
▼
┌─────────────────┐
│ Token[] (分词) │
└─────────────────┘
URL 输入: /users/123/posts
┌──────────────────────┐
│ parseURL 解析 URL │
│ path + query + hash │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 遍历 matchers 数组 │
│ 按分数降序排列 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ re.test(path) 测试 │
│ 找到第一个匹配 │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ matcher.parse() │
│ 提取 { id: '123' } │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ 构建 matched 数组 │
│ 从根到叶的记录链 │
└──────────────────────┘
User Router Matcher Guards History
│ │ │ │ │
│──push('/users')────────►│ │ │
│ │──resolve()─►│ │ │
│ │◄──location──│ │ │
│ │ │ │ │
│ │────navigate()─────────►│ │
│ │ │ │──leave───►│
│ │ │ │──each────►│
│ │ │ │──enter───►│
│ │◄───failure/void────────│ │
│ │ │ │ │
│ │────finalizeNavigation()───────────►│
│ │ │ │ │──push──►
│◄──Promise.resolve──────│ │ │
初始: /home
┌─────────────────────────────────────┐
│ back: null, current: /home, forward: null, position: 0 │
└─────────────────────────────────────┘
push('/about'):
┌─────────────────────────────────────┐
│ back: /home, current: /home, forward: /about, position: 0 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ back: /home, current: /about, forward: null, position: 1 │
└─────────────────────────────────────┘
go(-1) 回到 /home:
- 读取 position 0 的状态
- 恢复 currentLocation 为 /home
- 触发 popstate 监听器
匹配优化
运行时优化
关键:O(log n) 插入,O(n) 匹配(但通常很短)
function findInsertionIndex(
matcher: RouteRecordMatcher,
matchers: RouteRecordMatcher[]
): number {
let lower = 0, upper = matchers.length
while (lower !== upper) {
const mid = (lower + upper) >> 1 // 位运算除 2
const sortOrder = comparePathParserScore(matcher, matchers[mid])
if (sortOrder < 0) {
upper = mid // 新路由分数更高,往前插
} else {
lower = mid + 1 // 新路由分数更低,往后插
}
}
// 检查祖先同分情况
const insertionAncestor = getInsertionAncestor(matcher)
if (insertionAncestor) {
upper = matchers.lastIndexOf(insertionAncestor, upper - 1)
}
return upper
}
时间复杂度:O(log n) 查找 + O(n) 插入 = O(n)
// 懒加载组件
const routes = [
{
path: '/admin',
component: () => import('./Admin.vue'), // 动态导入
},
{
path: '/settings',
components: {
default: () => import('./Settings.vue'),
sidebar: () => import('./SettingsSidebar.vue'),
},
},
]
// 预加载提示
{
path: '/dashboard',
component: () => import(/* webpackPrefetch: true */ './Dashboard.vue'),
}
好处:代码分割、按需加载、减少首屏体积
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 1. 后退时恢复位置
if (savedPosition) return savedPosition
// 2. 锚点定位
if (to.hash) {
return { el: to.hash, behavior: 'smooth' }
}
// 3. 新页面滚动到顶部
return { top: 0, left: 0, behavior: 'smooth' }
},
})
// 滚动位置存储
saveScrollPosition(key, position) // 页面离开时
getSavedScrollPosition(key) // 后退时恢复
// 1. 命名路由
{ path: '/user/:id', name: 'user', component: User }
router.push({ name: 'user', params: { id: '123' } })
// 2. 嵌套路由
{
path: '/settings',
component: SettingsLayout,
children: [
{ path: '', name: 'settings-profile', component: Profile },
{ path: 'account', name: 'settings-account', component: Account },
],
}
// 3. 路由元信息
{
path: '/admin',
meta: { requiresAuth: true, roles: ['admin'] },
}
// 4. 动态导入 + 分组
const Admin = () => import(/* webpackChunkName: "admin" */ './Admin.vue')
避免
// ❌ 过深的嵌套
{
path: '/a',
children: [{
path: '/b',
children: [{
path: '/c',
children: [...] // 太深!
}]
}]
}
// ❌ 在守卫中直接修改状态
beforeEach((to) => {
store.user = null // ❌
})
推荐
// ✅ 扁平化路由
{
path: '/a/b/c',
component: C,
}
// ✅ 返回值控制导航
beforeEach(async (to) => {
await store.fetchUser()
if (!store.user) return '/login'
})
// 1. 动态添加路由
router.addRoute('admin', {
path: 'dashboard',
component: Dashboard,
})
// 2. 条件路由
const routes = [
{ path: '/public', component: Public },
]
if (isAdmin) {
routes.push({ path: '/admin', component: Admin })
}
// 3. 路由守卫中动态添加
router.beforeEach(async (to) => {
const permissions = await fetchPermissions()
permissions.forEach(p => {
router.addRoute({
path: p.path,
component: () => import(`./views/${p.component}.vue`),
})
})
})
// 1. 打印所有路由
console.log(router.getRoutes())
// 2. 解析 URL
const resolved = router.resolve('/users/123')
console.log(resolved.matched) // 匹配的路由链
console.log(resolved.params) // 解析的参数
// 3. 导航失败检测
router.push('/admin').then(failure => {
if (failure) {
console.log('导航失败:', failure.type)
// NAVIGATION_ABORTED
// NAVIGATION_CANCELLED
// NAVIGATION_DUPLICATED
}
})
// 4. DevTools
// 安装 Vue DevTools 查看 Router 面板
| 特性 | Vue Router | React Router | Angular Router |
|---|---|---|---|
| 嵌套路由 | ✅ children | ✅ Routes | ✅ children |
| 导航守卫 | ✅ 完整 | ⚠️ 组件内 | ✅ CanActivate |
| 路由懒加载 | ✅ import() | ✅ lazy | ✅ loadChildren |
| 滚动控制 | ✅ 内置 | ⚠️ 手动 | ✅ scroll |
| TypeScript | ✅ 原生 | ⚠️ 支持 | ✅ 原生 |
Vue Router 5
社区趋势
关注:unplugin-vue-router 提供类型安全的文件路由
核心要点
扩展阅读
访问地址:
https://atcfu.com/ai-articles/vue-router-matching/
2026-03-17 | 技术深度解读