💚 Vue Router 路由匹配

深度解析路由系统核心原理

基于源码深度解析
2026-03-17 | 技术深度解读

📑 目录

第一部分:基础架构

  • Vue Router 简介
  • 路由系统架构
  • 核心编译流程
  • 模块分层

第二部分:演进历史

  • 版本演进史
  • Vue Router 4 特性

第三部分:核心类解析

  • RouterMatcher / PathParser
  • Token / Tokenizer
  • RouterHistory

第四部分:核心函数

  • 路由匹配算法
  • 路径解析器
  • 评分系统

💚 Vue Router 简介

Vue Router 是 Vue.js 官方的路由管理器,为单页应用提供导航控制。

核心功能

  • 嵌套路由映射
  • 动态路由匹配
  • 模块化路由配置
  • 导航守卫
  • 细粒度的导航控制

技术特点

  • 基于 Vue 3 Composition API
  • TypeScript 完整支持
  • History API 封装
  • 自动编码/解码
  • 滚动行为控制

🏛️ 路由系统架构

┌─────────────────────────────────────────────────────────┐
│                      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 2
  • Options API 设计
  • Promise 基础导航
  • hash 模式默认

Vue Router 4.x

  • 基于 Vue 3
  • Composition API
  • 改进的类型推断
  • history 模式默认

重大变化:移除了 * 通配符,改用参数自定义正则

🚀 Vue Router 4 特性

// 动态路由参数正则
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 路径分词单元

🔗 RouterMatcher 接口

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
}

🎯 RouteRecordMatcher

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,扩展树形结构和别名支持

🔧 PathParser 接口

interface PathParser {
  // 用于匹配 URL 的正则
  re: RegExp

  // 评分数组(用于排序)
  score: Array

  // 路径中出现的参数键
  keys: PathParserParamKey[]

  // 解析 URL,返回参数对象
  // 无匹配返回 null
  parse(path: string): PathParams | null

  // 将参数对象转为 URL 字符串
  stringify(params: PathParams): string
}

核心:parse 和 stringify 实现双向转换

🔑 PathParserParamKey

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 }

🔤 Token 类型

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       // ? 或 *
}

📋 RouteRecordNormalized

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[]
}

📚 RouterHistory 接口

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
}

🗃️ StateEntry 状态

interface StateEntry extends HistoryState {
  // 后退位置
  back: HistoryLocation | null
  
  // 当前位置
  current: HistoryLocation
  
  // 前进位置
  forward: HistoryLocation | null
  
  // 历史栈位置
  position: number
  
  // 是否为替换操作
  replaced: boolean
  
  // 滚动位置
  scroll: _ScrollPositionNormalized | null | false
}

设计:完整的双向链表结构,支持前进后退导航

🎯 Router 核心接口

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

⚙️ createRouterMatcher

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,
  }
}

➕ addRoute 函数

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)
  }
}

🔍 resolve 函数

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) }
}

📊 insertMatcher 函数

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
}

✂️ tokenizePath 函数

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 │
                     └───────────────┘

特殊字符处理:
- \ : 转义下一个字符
- ( ) : 自定义正则
- * ? + : 参数修饰符

🔨 tokensToParser

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 }
}

📈 PathScore 评分系统

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,
}

规则:静态路径 > 动态参数,精确匹配 > 通配符

⚖️ comparePathParserScore

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
}

🏠 createRouter 函数

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
}

🚀 pushWithRedirect

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
    })
}

🛤️ navigate 函数

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 回调在导航确认后执行

🌐 createWebHistory

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
}

📤 push/replace 实现

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
}

👂 popstate 监听

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)

🎨 设计模式

工厂模式

  • createRouter
  • createRouterMatcher
  • createWebHistory

策略模式

  • 不同 History 实现
  • Hash / HTML5 / Memory

观察者模式

  • 导航守卫回调
  • popstate 监听
  • 响应式 currentRoute

组合模式

  • 嵌套路由树
  • RouteRecordMatcher 树

📐 路由系统 UML

┌──────────────┐      ┌───────────────┐
│   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──────│           │           │

📚 History 状态流转

初始: /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 监听器

⚡ 性能优化策略

匹配优化

  • 二分查找插入位置
  • 正则预编译
  • 评分排序缓存
  • 名称映射快速查找

运行时优化

  • shallowRef 响应式
  • 守卫去重
  • 延迟组件解析
  • 滚动位置缓存

关键: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

  • Data Loaders
  • 更好的 SSR 支持
  • 改进的类型推断
  • 更小的包体积

社区趋势

  • 基于文件的路由
  • 类型安全路由
  • 服务端组件集成
  • 岛屿架构支持

关注:unplugin-vue-router 提供类型安全的文件路由

📚 总结与扩展阅读

核心要点

  • 路径分词 → 正则生成 → 评分排序 → 匹配解析
  • 嵌套路由通过树形结构实现
  • 导航守卫按严格顺序执行
  • History API 完整封装,支持状态管理

扩展阅读

  • Vue Router 官方文档:router.vuejs.org
  • 源码仓库:github.com/vuejs/router
  • RFC 文档:github.com/vuejs/rfcs
  • Path-to-RegExp:paths.esm.dev

💚 感谢观看

Vue Router 路由匹配源码解读

访问地址:
https://atcfu.com/ai-articles/vue-router-matching/

2026-03-17 | 技术深度解读