第三章:掌握路由匹配与路径解析
在前两章中,我们深入探讨了 Vue Router 的本质和路由模式实现。现在,让我们深入了解 Vue Router 的核心功能之一——路由匹配与路径解析。我们将学习路径匹配算法、路由记录结构、嵌套路由匹配等关键概念。
路径匹配算法:从简单到复杂
Vue Router 的路径匹配是其核心功能之一。它需要能够处理静态路径、动态路径参数、通配符等多种情况。
path-to-regexp 的替代:手写路径解析器
虽然 Vue Router 内部使用了类似 path-to-regexp 的库,但我们可以通过手写一个简化版本来理解其工作原理:
javascript
// 简化的路径解析器实现
class PathParser {
constructor(path) {
this.path = path
this.keys = [] // 存储动态参数的键
this.regexp = this.compilePath(path)
}
// 编译路径为正则表达式
compilePath(path) {
// 转义特殊字符
let regexpSource = path
.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
.replace(/\/:([A-Za-z0-9_]+)/g, (_, key) => {
// 动态参数处理
this.keys.push({ name: key, optional: false })
return '/([^/]+)'
})
.replace(/\/:([A-Za-z0-9_]+)\?/g, (_, key) => {
// 可选参数处理
this.keys.push({ name: key, optional: true })
return '(?:/([^/]+))?'
})
.replace(/\/\*/g, () => {
// 通配符处理
this.keys.push({ name: 'pathMatch', optional: false })
return '/(.*)'
})
// 确保完整匹配
regexpSource = '^' + regexpSource + '(?:\\?.*)?$'
return new RegExp(regexpSource)
}
// 匹配路径并提取参数
match(path) {
const match = this.regexp.exec(path)
if (!match) return null
const params = {}
this.keys.forEach((key, index) => {
const value = match[index + 1]
if (value !== undefined) {
params[key.name] = value
}
})
return { path, params }
}
}
// 使用示例
const parser = new PathParser('/user/:id/posts/:postId')
console.log(parser.match('/user/123/posts/456'))
// 输出: { path: '/user/123/posts/456', params: { id: '123', postId: '456' } }
const optionalParser = new PathParser('/user/:id?')
console.log(optionalParser.match('/user')) // { path: '/user', params: {} }
console.log(optionalParser.match('/user/123')) // { path: '/user/123', params: { id: '123' } }RouteRecord 结构:标准化的路由记录表示
Vue Router 使用 RouteRecord 来表示路由配置,这是一种标准化的数据结构:
javascript
// RouteRecord 结构定义
interface RouteRecord {
// 路径
path: string
// 路由名称(可选)
name?: string | symbol
// 组件(单视图路由)
component?: Component
// 组件映射(多视图路由)
components?: Record<string, Component>
// 重定向配置
redirect?: RouteRecordRedirectOption
// 子路由
children?: RouteRecordRaw[]
// 元信息
meta?: RouteMeta
// 路由独享守卫
beforeEnter?: NavigationGuard | NavigationGuard[]
// Props 配置
props?: boolean | Record<string, any> | RoutePropsFunction
}
// 路由记录示例
const routeRecords = [
{
path: '/',
name: 'Home',
component: HomeComponent,
meta: { requiresAuth: false }
},
{
path: '/user/:id',
name: 'User',
component: UserComponent,
children: [
{
path: 'profile',
name: 'UserProfile',
component: UserProfileComponent
},
{
path: 'posts',
name: 'UserPosts',
component: UserPostsComponent
}
],
meta: { requiresAuth: true },
beforeEnter: (to, from, next) => {
// 路由独享守卫
if (checkUserPermission(to.params.id)) {
next()
} else {
next('/login')
}
}
},
{
path: '/admin',
components: {
default: AdminComponent,
sidebar: AdminSidebarComponent
},
meta: { requiresAuth: true, role: 'admin' }
}
]匹配优先级:最长前缀匹配 vs 注册顺序
Vue Router 在匹配路由时需要考虑匹配优先级,确保正确的路由被选中:
javascript
// 路由匹配优先级实现
class RouteMatcher {
constructor(routes) {
this.records = []
routes.forEach(route => this.addRoute(route))
}
// 添加路由记录
addRoute(route) {
const record = normalizeRouteRecord(route)
this.records.push(record)
}
// 匹配路由
match(location) {
const { path, query, hash } = location
const matched = []
// 按优先级排序路由记录
const sortedRecords = this.sortByPriority(this.records)
for (const record of sortedRecords) {
const matchResult = this.matchRecord(record, path)
if (matchResult) {
matched.push({
...record,
params: matchResult.params
})
// 如果是完全匹配,停止继续匹配
if (matchResult.isExact) {
break
}
}
}
return {
path,
query,
hash,
matched,
params: matched.reduce((params, record) => {
return { ...params, ...record.params }
}, {})
}
}
// 匹配单个路由记录
matchRecord(record, path) {
const parser = new PathParser(record.path)
const match = parser.match(path)
if (match) {
// 检查是否是完全匹配
const isExact = match.path === path ||
(record.path === '/' && match.path === '')
return {
params: match.params,
isExact
}
}
return null
}
// 按优先级排序
sortByPriority(records) {
return [...records].sort((a, b) => {
// 1. 静态路径优先于动态路径
const aIsStatic = !a.path.includes(':') && !a.path.includes('*')
const bIsStatic = !b.path.includes(':') && !b.path.includes('*')
if (aIsStatic && !bIsStatic) return -1
if (!aIsStatic && bIsStatic) return 1
// 2. 更具体的路径优先(路径更长)
const aLength = a.path.split('/').length
const bLength = b.path.split('/').length
if (aLength !== bLength) {
return bLength - aLength
}
// 3. 按注册顺序
return records.indexOf(a) - records.indexOf(b)
})
}
}
// 匹配优先级示例
const routes = [
{ path: '/user/profile', component: UserProfile },
{ path: '/user/:id', component: User },
{ path: '/user/*', component: UserWildcard },
{ path: '/:pathMatch(.*)*', component: NotFound }
]
const matcher = new RouteMatcher(routes)
console.log(matcher.match({ path: '/user/profile' }))
// 匹配到 /user/profile,而不是 /user/:id
console.log(matcher.match({ path: '/user/123' }))
// 匹配到 /user/:id
console.log(matcher.match({ path: '/user/123/posts' }))
// 匹配到 /user/*
console.log(matcher.match({ path: '/unknown' }))
// 匹配到通配符路由嵌套路由:生成嵌套的 matched 数组
嵌套路由是 Vue Router 的重要特性,它允许创建复杂的路由层次结构:
javascript
// 嵌套路由匹配实现
class NestedRouteMatcher {
constructor(routes) {
this.records = this.normalizeRoutes(routes)
}
// 标准化路由配置
normalizeRoutes(routes, parent) {
const records = []
routes.forEach(route => {
const record = {
...route,
parent, // 父路由记录
path: this.normalizePath(route.path, parent)
}
records.push(record)
// 递归处理子路由
if (route.children) {
const childRecords = this.normalizeRoutes(route.children, record)
records.push(...childRecords)
}
})
return records
}
// 标准化路径
normalizePath(path, parent) {
if (path[0] === '/') {
// 绝对路径
return path
}
if (parent == null) {
// 根路径
return path
}
// 相对路径,拼接父路径
return (parent.path + '/' + path).replace(/\/+/g, '/')
}
// 匹配嵌套路由
match(location) {
const { path } = location
const matchedRecords = []
// 查找所有匹配的路由记录
const matchedPaths = this.findMatchedPaths(path)
// 构建匹配记录链
let currentRecord = null
for (const path of matchedPaths) {
const record = this.findRecordByPath(path, currentRecord)
if (record) {
matchedRecords.push(record)
currentRecord = record
}
}
// 提取参数
const params = this.extractParams(path, matchedRecords)
return {
...location,
matched: matchedRecords,
params
}
}
// 查找匹配的路径
findMatchedPaths(path) {
const paths = []
let currentPath = ''
// 分割路径并逐级匹配
const segments = path.split('/').filter(Boolean)
segments.unshift('') // 添加根路径
for (let i = 0; i < segments.length; i++) {
if (i === 0) {
currentPath = segments[i] === '' ? '/' : '/' + segments[i]
} else {
currentPath += (currentPath === '/' ? '' : '/') + segments[i]
}
// 处理可选参数和动态路径
if (this.hasRecordForPath(currentPath)) {
paths.push(currentPath)
}
}
return paths
}
// 根据路径查找记录
findRecordByPath(path, parent) {
return this.records.find(record => {
if (record.parent !== parent) {
return false
}
const parser = new PathParser(record.path)
return parser.match(path) !== null
})
}
// 检查路径是否有对应的记录
hasRecordForPath(path) {
return this.records.some(record => {
const parser = new PathParser(record.path)
return parser.match(path) !== null
})
}
// 提取参数
extractParams(path, records) {
const params = {}
records.forEach(record => {
const parser = new PathParser(record.path)
const match = parser.match(path)
if (match && match.params) {
Object.assign(params, match.params)
}
})
return params
}
}
// 嵌套路由示例
const routes = [
{
path: '/user',
component: User,
children: [
{
path: 'profile',
component: UserProfile
},
{
path: 'posts',
component: UserPosts,
children: [
{
path: ':postId',
component: UserPost
}
]
}
]
}
]
const matcher = new NestedRouteMatcher(routes)
console.log(matcher.match({ path: '/user/profile' }))
// matched: [UserRecord, UserProfileRecord]
console.log(matcher.match({ path: '/user/posts/123' }))
// matched: [UserRecord, UserPostsRecord, UserPostRecord]命名视图:支持多 router-view
Vue Router 支持命名视图,允许在同一个路由下渲染多个组件:
javascript
// 命名视图实现
const routes = [
{
path: '/',
components: {
default: Home,
sidebar: Sidebar,
footer: Footer
}
},
{
path: '/dashboard',
component: Dashboard, // 隐式使用 default 名称
children: [
{
path: 'stats',
components: {
default: Stats,
sidebar: DashboardSidebar
}
}
]
}
]
// RouterView 组件处理命名视图
const RouterView = {
name: 'RouterView',
props: {
name: {
type: String,
default: 'default'
}
},
setup(props) {
const route = inject('route')
const depth = inject('routerViewDepth', 0)
// 获取当前层级的匹配记录
const matchedRoute = computed(() => {
return route.matched[depth]
})
// 获取要渲染的组件
const ViewComponent = computed(() => {
const record = matchedRoute.value
if (!record) return null
// 处理命名视图
if (record.components) {
return record.components[props.name]
}
// 处理单组件视图
if (props.name === 'default' && record.component) {
return record.component
}
return null
})
// 提供下一级深度
provide('routerViewDepth', depth + 1)
return () => {
if (!ViewComponent.value) {
return h('!--router-view--')
}
return h(ViewComponent.value)
}
}
}normalizeLocation 与 resolve
Vue Router 提供了 resolve 方法来标准化路由位置:
javascript
// 路由位置标准化实现
function normalizeLocation(to, current, append, router) {
// 字符串路径
if (typeof to === 'string') {
return {
path: to,
params: {},
query: {},
hash: ''
}
}
// 路由位置对象
if (typeof to === 'object') {
const normalized = { ...to }
// 处理命名路由
if (normalized.name) {
const record = router.matcher.getRecordByName(normalized.name)
if (record) {
// 填充路径参数
if (normalized.params) {
normalized.path = fillParams(record.path, normalized.params)
}
}
}
return normalized
}
return to
}
// 参数填充实现
function fillParams(path, params) {
return path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
const value = params[key]
if (value == null) {
throw new Error(`Missing param for ${key}`)
}
return encodeURIComponent(value)
})
}
// resolve 方法实现
function resolve(to, currentLocation) {
// 标准化位置
const location = normalizeLocation(to, currentLocation)
// 匹配路由
const route = this.matcher.match(location)
// 生成完整 URL
const href = this.history.base + route.path
return {
...route,
href,
redirectedFrom: undefined
}
}params 填充与 query 序列化
Vue Router 需要处理路由参数和查询参数:
javascript
// 参数处理工具
class RouteUtils {
// 填充路径参数
static fillParams(path, params) {
let pathWithParams = path
// 替换动态参数
Object.keys(params).forEach(key => {
const value = params[key]
const encodedValue = encodeURIComponent(value)
pathWithParams = pathWithParams.replace(
new RegExp(`:${key}(\\(.*?\\))?`, 'g'),
encodedValue
)
})
// 处理可选参数
pathWithParams = pathWithParams.replace(/\/:\w+\?/g, '')
return pathWithParams
}
// 序列化查询参数
static stringifyQuery(query) {
const searchParams = new URLSearchParams()
Object.keys(query).forEach(key => {
const value = query[key]
if (Array.isArray(value)) {
value.forEach(val => {
searchParams.append(key, val)
})
} else if (value != null) {
searchParams.append(key, value)
}
})
const querystring = searchParams.toString()
return querystring ? `?${querystring}` : ''
}
// 解析查询参数
static parseQuery(querystring) {
const query = {}
if (!querystring) return query
// 移除开头的 ?
const search = querystring.charAt(0) === '?'
? querystring.slice(1)
: querystring
const searchParams = new URLSearchParams(search)
for (const [key, value] of searchParams) {
if (query[key]) {
// 处理数组参数
if (Array.isArray(query[key])) {
query[key].push(value)
} else {
query[key] = [query[key], value]
}
} else {
query[key] = value
}
}
return query
}
}
// 使用示例
const path = '/user/:id/posts/:postId'
const params = { id: '123', postId: '456' }
const filledPath = RouteUtils.fillParams(path, params)
console.log(filledPath) // /user/123/posts/456
const query = { page: '1', filter: ['active', 'featured'] }
const querystring = RouteUtils.stringifyQuery(query)
console.log(querystring) // ?page=1&filter=active&filter=featured
const parsedQuery = RouteUtils.parseQuery('?page=1&filter=active&filter=featured')
console.log(parsedQuery) // { page: '1', filter: ['active', 'featured'] }小结
在本章中,我们深入探讨了 Vue Router 的路由匹配与路径解析机制:
- 路径匹配算法 - 从简单到复杂,支持静态路径、动态参数、通配符
- RouteRecord 结构 - 标准化的路由记录表示
- 匹配优先级 - 静态路径优先、更长路径优先、注册顺序优先
- 嵌套路由 - 生成嵌套的 matched 数组
- 命名视图 - 支持同一路由下渲染多个组件
- 位置标准化 - normalizeLocation 和 resolve 方法
- 参数处理 - params 填充和 query 序列化
这些机制共同构成了 Vue Router 强大的路由匹配能力。
在下一章中,我们将深入探讨导航流程与守卫机制,包括导航守卫的执行顺序和异步流程控制。
思考题:
- 你在项目中是否遇到过路由匹配不符合预期的情况?是如何调试和解决的?
- 对于复杂的嵌套路由结构,你通常如何组织和管理路由配置?