Skip to content

第三章:掌握路由匹配与路径解析

在前两章中,我们深入探讨了 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)
    }
  }
}

normalizeLocationresolve

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 的路由匹配与路径解析机制:

  1. 路径匹配算法 - 从简单到复杂,支持静态路径、动态参数、通配符
  2. RouteRecord 结构 - 标准化的路由记录表示
  3. 匹配优先级 - 静态路径优先、更长路径优先、注册顺序优先
  4. 嵌套路由 - 生成嵌套的 matched 数组
  5. 命名视图 - 支持同一路由下渲染多个组件
  6. 位置标准化 - normalizeLocation 和 resolve 方法
  7. 参数处理 - params 填充和 query 序列化

这些机制共同构成了 Vue Router 强大的路由匹配能力。

在下一章中,我们将深入探讨导航流程与守卫机制,包括导航守卫的执行顺序和异步流程控制。


思考题

  1. 你在项目中是否遇到过路由匹配不符合预期的情况?是如何调试和解决的?
  2. 对于复杂的嵌套路由结构,你通常如何组织和管理路由配置?