vue-router v5.x createRouterMatcher 是如何创建路由匹配器?

5 阅读9分钟

vue-router v5.x(5.0.3) 要求 pinia v3.x

createRouterMatcher

1、做了什么?

初始化路由匹配系统,定义路由管理方法。

2、返回了什么?

  • addRoute,注册路由。
  • resolve,解析路由。
  • removeRoute,移除路由。
  • clearRoutes,清除所有路由。
  • getRoutes,获取所有注册的路由。
  • getRecordMatcher,根据路由名称获取匹配器。
/**
 * Creates a Router Matcher.
 * 创建路由匹配器
 * @internal
 * @param routes - array of initial routes 初始路由配置数组
 * @param globalOptions - global route options 原始配置
 */
export function createRouterMatcher(
  routes: Readonly<RouteRecordRaw[]>, 
  globalOptions: PathParserOptions 
): RouterMatcher {

  // normalized ordered array of matchers
  // 存储所有路由匹配器的数组(按优先级排序)
  const matchers: RouteRecordMatcher[] = []
  
  // 按路由名称索引的匹配器映射表
  const matcherMap = new Map<
    NonNullable<RouteRecordNameGeneric>,
    RouteRecordMatcher
  >()
  
  globalOptions = mergeOptions<PathParserOptions>(
    // 合并全局选项与默认选项,确保所有选项都有默认值
    PATH_PARSER_OPTIONS_DEFAULTS,
    globalOptions
  )

  function getRecordMatcher(name: NonNullable<RouteRecordNameGeneric>) {
    return matcherMap.get(name)
  }

  function getRoutes() {
    return matchers
  }

  /**
   * 将新的路由匹配器(RouteRecordMatcher)插入到匹配器数组(matchers)的正确优先级位置
   * @param matcher 新的路由匹配器
   */
  function insertMatcher(matcher: RouteRecordMatcher) {
    // 根据路由的 score(优先级分数)计算新匹配器在 matchers 数组中的插入位置
    const index = findInsertionIndex(matcher, matchers)
    matchers.splice(index, 0, matcher)
    // only add the original record to the name map
    // 排除别名记录,仅添加原始记录到名称映射
    // 原因?若别名路由也注册名称,会导致「一个名称对应多个匹配器」,引发命名冲突。
    if (matcher.record.name && !isAliasRecord(matcher))
      matcherMap.set(matcher.record.name, matcher)
  }

  // add initial routes
  routes.forEach(route => addRoute(route))

  function clearRoutes() {
    matchers.length = 0
    matcherMap.clear()
  }

  return {
    addRoute,
    resolve,
    removeRoute,
    clearRoutes,
    getRoutes,
    getRecordMatcher,
  }
}

内部函数 addRoute

  /**
   * 动态添加路由
   * @param record  要添加的原始路由配置
   * @param parent  父路由匹配器(用于嵌套路由)
   * @param originalRecord  原始路由匹配器(用于别名关联)
   * @returns 路由匹配器的删除函数
   */
  function addRoute(
    record: RouteRecordRaw,
    parent?: RouteRecordMatcher,
    originalRecord?: RouteRecordMatcher
  ) {
    // used later on to remove by name
    const isRootAdd = !originalRecord // 标记是否为顶级路由添加(非别名)
    // 规范化路由记录(处理组件、路径等基础信息)
    const mainNormalizedRecord = normalizeRouteRecord(record)
    if (__DEV__) {
      checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent)
    }
    // we might be the child of an alias
    // 标记当前路由是否为某个路由的别名
    mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record

    const options: PathParserOptions = mergeOptions(globalOptions, record)

    // generate an array of records to correctly handle aliases
    const normalizedRecords: RouteRecordNormalized[] = [mainNormalizedRecord]

    // 处理别名:为每个别名创建规范化记录
    if ('alias' in record) {
      const aliases =
        typeof record.alias === 'string' ? [record.alias] : record.alias!
        // 若路由配置了 alias(别名),将「原始路由 + 所有别名」转换为多个标准化路由记录,存入 normalizedRecords 数组
      for (const alias of aliases) {
        normalizedRecords.push(
          // we need to normalize again to ensure the `mods` property
          // being non enumerable
          normalizeRouteRecord(
            assign({}, mainNormalizedRecord, {
              // this allows us to hold a copy of the `components` option
              // so that async components cache is hold on the original record
              components: originalRecord
                ? originalRecord.record.components
                : mainNormalizedRecord.components,
              path: alias, // 别名路径
              // we might be the child of an alias
              // 若原始路由有 aliasOf 字段(别名关联),则当前别名路由也关联到该原始路由
              aliasOf: originalRecord
                ? originalRecord.record
                : mainNormalizedRecord,
              // the aliases are always of the same kind as the original since they
              // are defined on the same record
            })
          )
        )
      }
    }

    let matcher: RouteRecordMatcher // 当前处理的路由匹配器
    let originalMatcher: RouteRecordMatcher | undefined // 原始路由的匹配器(非别名)

    // 处理所有规范化记录(原始路由 + 别名路由)
    for (const normalizedRecord of normalizedRecords) {
      const { path } = normalizedRecord
      // Build up the path for nested routes if the child isn't an absolute
      // route. Only add the / delimiter if the child path isn't empty and if the
      // parent path doesn't have a trailing slash
      // 嵌套路由拼接路径
      // 仅当「存在父路由(parent)」且「当前路径不是绝对路径(不以 / 开头)」时拼接
      if (parent && path[0] !== '/') {
        const parentPath = parent.record.path

        // 处理父路径末尾的斜杠:
        // 若父路径以 / 结尾(如 /user/),则不加斜杠;
        // 否则加 /(如父 /user + 子 profile → /user/profile)
        const connectingSlash =
          parentPath[parentPath.length - 1] === '/' ? '' : '/'

        normalizedRecord.path =
          parent.record.path + (path && connectingSlash + path)
      }

      // 禁止使用 * 通配符路由
      // 通配符路由(*)已被移除,需使用自定义正则表达式参数替代
      if (__DEV__ && normalizedRecord.path === '*') {
        throw new Error(
          'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
            'See more at https://router.vuejs.org/guide/migration/#Removed-star-or-catch-all-routes.'
        )
      }

      // create the object beforehand, so it can be passed to children
      // 创建路由匹配器
      matcher = createRouteRecordMatcher(normalizedRecord, parent, options)

      if (__DEV__ && parent && path[0] === '/')
        checkMissingParamsInAbsolutePath(matcher, parent)

      // if we are an alias we must tell the original record that we exist,
      // so we can be removed
      // 别名关联:别名匹配器加入原始匹配器的 alias 数组,便于删除时递归清理
      if (originalRecord) {
        originalRecord.alias.push(matcher)
        if (__DEV__) {
          checkSameParams(originalRecord, matcher)
        }
      } else {
        // otherwise, the first record is the original and others are aliases
        originalMatcher = originalMatcher || matcher
        if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)

        // remove the route if named and only for the top record (avoid in nested calls)
        // this works because the original record is the first one
        if (isRootAdd && record.name && !isAliasRecord(matcher)) {
          if (__DEV__) {
            checkSameNameAsAncestor(record, parent)
          }
          removeRoute(record.name)
        }
      }

      // Avoid adding a record that doesn't display anything. This allows passing through records without a component to
      // not be reached and pass through the catch all route
      // 插入匹配器到路由系统
      if (isMatchable(matcher)) {
        insertMatcher(matcher)
      }

      // 递归处理嵌套子路由
      if (mainNormalizedRecord.children) {
        const children = mainNormalizedRecord.children
        for (let i = 0; i < children.length; i++) {
          addRoute(
            children[i],
            matcher,
            originalRecord && originalRecord.children[i]
          )
        }
      }

      // if there was no original record, then the first one was not an alias and all
      // other aliases (if any) need to reference this record when adding children
      originalRecord = originalRecord || matcher

      // TODO: add normalized records for more flexibility
      // if (parent && isAliasRecord(originalRecord)) {
      //   parent.children.push(originalRecord)
      // }
    }

    return originalMatcher
      ? () => {
          // since other matchers are aliases, they should be removed by the original matcher
          removeRoute(originalMatcher!)
        }
      : noop
  }

image.png

内部函数 removeRoute

  /**
   * 支持通过「路由名称」或「路由匹配器对象」两种方式删除路由,同时递归清理子路由、别名路
   * @param matcherRef 
   */
  function removeRoute(
    // RouteRecordNameGeneric:路由名称(字符串 / 符号,如 'user');
    // RouteRecordMatcher:路由匹配器对象(包含 score/re/record/children/alias 等字段);
    matcherRef: NonNullable<RouteRecordNameGeneric> | RouteRecordMatcher
  ) {

    // 分支 1:入参是「路由名称」
    if (isRouteName(matcherRef)) {
      const matcher = matcherMap.get(matcherRef)
      if (matcher) {
        matcherMap.delete(matcherRef) // 移除名称→匹配器的映射
        matchers.splice(matchers.indexOf(matcher), 1) // 从匹配器数组删除
        matcher.children.forEach(removeRoute) // 递归删除子路由
        matcher.alias.forEach(removeRoute) // 递归删除别名路由
      }

      // 分支 2:入参是「路由匹配器对象」
    } else {
      const index = matchers.indexOf(matcherRef)
      if (index > -1) {
        matchers.splice(index, 1)
        if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name)
        matcherRef.children.forEach(removeRoute)
        matcherRef.alias.forEach(removeRoute)
      }
    }
  }

内部函数 resolve

  /**
   * 将开发者传入的「原始导航目标」解析为标准化的 MatcherLocation 对象
   * @param location 原始导航目标,如 { name: 'user', params: { id: 1 } } 或 /user/1
   * @param currentLocation 当前路由位置,用于解析相对路径
   * @returns 标准化的 MatcherLocation 对象,包含 path、name、params 等字段
   */
  function resolve(
    location: Readonly<MatcherLocationRaw>,
    currentLocation: Readonly<MatcherLocation>
  ): MatcherLocation {
    let matcher: RouteRecordMatcher | undefined
    let params: PathParams = {}
    let path: MatcherLocation['path']
    let name: MatcherLocation['name']

    // 核心分支 1:按「路由名称」解析(
    if ('name' in location && location.name) {
      matcher = matcherMap.get(location.name)

      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
        })

      // warn if the user is passing invalid params so they can debug it better when they get removed
      if (__DEV__) {
        const invalidParams: string[] = Object.keys(
          location.params || {}
        ).filter(paramName => !matcher!.keys.find(k => k.name === paramName))

        if (invalidParams.length) {
          warn(
            `Discarded invalid param(s) "${invalidParams.join(
              '", "'
            )}" when navigating. See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`
          )
        }
      }

      name = matcher.record.name
      params = assign(
        // paramsFromLocation is a new object
        pickParams(
          currentLocation.params,
          // only keep params that exist in the resolved location
          // only keep optional params coming from a parent record
          matcher.keys
            .filter(k => !k.optional)
            .concat(
              matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []
            )
            .map(k => k.name)
        ),
        // discard any existing params in the current location that do not exist here
        // #1497 this ensures better active/exact matching
        location.params &&
          pickParams(
            location.params,
            matcher.keys.map(k => k.name)
          )
      )
      // throws if cannot be stringified
      path = matcher.stringify(params)


      //  核心分支 2:按「路径」解析(
    } else if (location.path != null) {
      // no need to resolve the path with the matcher as it was provided
      // this also allows the user to control the encoding
      path = location.path

      if (__DEV__ && !path.startsWith('/')) {
        warn(
          `The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://github.com/vuejs/router/issues/new/choose.`
        )
      }

      matcher = matchers.find(m => m.re.test(path))
      // matcher should have a value after the loop

      if (matcher) {
        // we know the matcher works because we tested the regexp
        params = matcher.parse(path)!
        name = matcher.record.name
      }
      // location is a relative path
    } else {
      // match by name or path of current route
      // 核心分支 3:兜底解析(无 name/path
      // 优先按当前路由名称找匹配器,否则按当前路径匹配
      matcher = currentLocation.name
        ? matcherMap.get(currentLocation.name)
        : matchers.find(m => m.re.test(currentLocation.path))
      if (!matcher)
        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
          location,
          currentLocation,
        })
      name = matcher.record.name
      // since we are navigating to the same location, we don't need to pick the
      // params like when `name` is provided
      params = assign({}, currentLocation.params, location.params)
      path = matcher.stringify(params)
    }

    // 收尾:构建 matched 路由记录数组 
    const matched: MatcherLocation['matched'] = []
    let parentMatcher: RouteRecordMatcher | undefined = matcher
    while (parentMatcher) {
      // reversed order so parents are at the beginning

      matched.unshift(parentMatcher.record)
      parentMatcher = parentMatcher.parent
    }

    return {
      name,
      path,
      params,
      matched,
      meta: mergeMetaFields(matched),
    }
  }

createRouteRecordMatcher

/**
 * 将「标准化路由记录(RouteRecord)」转换为「路由匹配器(RouteRecordMatcher)」
 * @param record 标准化路由记录
 * @param parent 父路由匹配器
 * @param options 路径解析选项
 * @returns 路由记录匹配器
 */
export function createRouteRecordMatcher(
  record: Readonly<RouteRecord>,
  parent: RouteRecordMatcher | undefined,
  options?: PathParserOptions
): RouteRecordMatcher {

  // tokenizePath 拆分静态路径段和动态参数段
  const parser = tokensToParser(tokenizePath(record.path), options)

  // warn against params with the same name
  // 开发环境校验重复参数名
  if (__DEV__) {
    const existingKeys = new Set<string>()
    for (const key of parser.keys) {
      if (existingKeys.has(key.name))
        warn(
          `Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`
        )
      existingKeys.add(key.name)
    }
  }

  // 构建路由匹配器对象
  const matcher: RouteRecordMatcher = assign(parser, {
    record,
    parent,
    // these needs to be populated by the parent
    children: [],
    alias: [],
  })

  // 关联父匹配器的子列表(嵌套场景)
  if (parent) {
    // both are aliases or both are not aliases
    // we don't want to mix them because the order is used when
    // passing originalRecord in Matcher.addRoute
    if (!matcher.record.aliasOf === !parent.record.aliasOf)
      parent.children.push(matcher)
  }

  return matcher
}
{
  path: '/lists/:type?',
  name: 'lists',
  component: () => import('@/views/lists/ListView.vue')
},
re = /^\/lists(?:\/([^/]+?))?\/?$/i

image.png

image.png

tokensToParser

/**
 * Creates a path parser from an array of Segments (a segment is an array of Tokens)
 * 将「路径令牌二维数组(Token[][])」转换为「路径解析器(PathParser)」
 *
 * @param segments - array of segments returned by tokenizePath
 * @param extraOptions - optional options for the regexp
 * @returns a PathParser
 */
export function tokensToParser(
  segments: Array<Token[]>,
  extraOptions?: _PathParserOptions
): PathParser {
  // 合并默认选项和用户传入选项
  const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)

  // the amount of scores is the same as the length of segments except for the root segment "/"
  // 存储每个路径段 / 令牌的匹配分数,用于多路由匹配时的优先级排序(如静态路由 /user 优先级高于动态路由 /:id)
  const score: Array<number[]> = []
  // the regexp as a string
  let pattern = options.start ? '^' : ''
  // extracted keys
  const keys: PathParserParamKey[] = []

  // 遍历路径段生成正则与分数
  for (const segment of segments) {
    // the root segment needs special treatment
    // 根路径段(空数组)特殊处理:分数初始化为 [PathScore.Root]
    const segmentScores: number[] = segment.length ? [] : [PathScore.Root]

    // allow trailing slash
     // 严格模式下,空段(末尾斜杠)添加 / 到正则
    if (options.strict && !segment.length) pattern += '/'

    // 遍历当前段的所有令牌
    for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {

      const token = segment[tokenIndex]

      // resets the score if we are inside a sub-segment /:a-other-:b
      // 初始化子段分数:基础分 + 区分大小写加分
      let subSegmentScore: number =
        PathScore.Segment +
        // 配置 区分大小写 时,添加额外分数,提升优先级 0.25 
        (options.sensitive ? PathScore.BonusCaseSensitive : 0)

      // 1、静态令牌(如 /user):基础分 + 静态令牌分
      if (token.type === TokenType.Static) {
        // prepend the slash if we are starting a new segment
        if (!tokenIndex) pattern += '/'  // 新段开头添加 /(避免拼接出 // 等无效路径)

        // 转义正则特殊字符后拼接到正则字符串
        pattern += token.value.replace(REGEX_CHARS_RE, '\\$&')
        subSegmentScore += PathScore.Static // 静态令牌加分,提升优先级 40

        // 2、动态参数
      } else if (token.type === TokenType.Param) {

        const { value, repeatable, optional, regexp } = token
        keys.push({
          name: value,
          repeatable,
          optional,
        })
        const re = regexp ? regexp : BASE_PARAM_PATTERN

        // the user provided a custom regexp /:id(\\d+)
        // 自定义正则表达式
        if (re !== BASE_PARAM_PATTERN) {
          subSegmentScore += PathScore.BonusCustomRegExp // 提升优先级 10
          // make sure the regexp is valid before using it
          try {
            // 校验正则合法性,非法则抛错
            new RegExp(`(${re})`)
          } catch (err) {
            throw new Error(
              `Invalid custom RegExp for param "${value}" (${re}): ` +
                (err as Error).message
            )
          }
        }

        // when we repeat we must take care of the repeating leading slash
        // 可重复参数(如 /:w+):添加 (?:/(?:${re}))* 匹配多个子路径段
        let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`

        // prepend the slash if we are starting a new segment
        // 开头处理
        if (!tokenIndex)
          subPattern =
            // avoid an optional / if there are more segments e.g. /:p?-static
            // or /:p?-:p2
            optional && segment.length < 2
            // 可选参数且段内只有一个令牌 → 包裹非捕获组(避免多余 /)
              ? `(?:/${subPattern})`  // 如 /:id? → (?:/(\d+))?
              : '/' + subPattern 

        if (optional) subPattern += '?'

        pattern += subPattern

        subSegmentScore += PathScore.Dynamic // 动态参数加分,提升优先级 20
        
        if (optional) subSegmentScore += PathScore.BonusOptional // 可选参数加分,降低优先级 -8
        if (repeatable) subSegmentScore += PathScore.BonusRepeatable // 可重复参数加分,降低优先级 -20
        if (re === '.*') subSegmentScore += PathScore.BonusWildcard // 通配符参数加分,降低优先级 -50
      }

      segmentScores.push(subSegmentScore) // 将当前令牌分数加入段分数数组
    }

    // an empty array like /home/ -> [[{home}], []]
    // if (!segment.length) pattern += '/'

    score.push(segmentScores) // 将当前段分数加入总分数数组
  }

  // only apply the strict bonus to the last score
  // 严格模式下,给最后一个分数添加严格模式加分
  if (options.strict && options.end) {
    const i = score.length - 1
    score[i][score[i].length - 1] += PathScore.BonusStrict
  }

  // TODO: dev only warn double trailing slash
  // 非严格模式下,允许末尾斜杠(添加 /?)
  if (!options.strict) pattern += '/?'

  // 结束符处理:end=true → 添加 $
  if (options.end) pattern += '$'
  // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
  // 否则处理为 (?:/|$)(匹配末尾或斜杠)
  else if (options.strict && !pattern.endsWith('/')) pattern += '(?:/|$)'

  const re = new RegExp(pattern, options.sensitive ? '' : 'i')

  // 实现 parse 方法(URL → 参数)
  function parse(path: string): PathParams | null {
    const match = path.match(re)
    const params: PathParams = {}

    if (!match) return null

    // 遍历匹配结果(跳过第 0 项,第 0 项是完整匹配)
    for (let i = 1; i < match.length; i++) {
      const value: string = match[i] || ''
      const key = keys[i - 1]
      // 可重复参数 → 拆分为数组;否则直接赋值
      params[key.name] = value && key.repeatable ? value.split('/') : value
    }

    return params
  }

  // 实现 stringify 方法(参数 → URL)
  function stringify(params: PathParams): string {
    let path = ''
    // for optional parameters to allow to be empty
    let avoidDuplicatedSlash: boolean = false // 避免重复斜杠的标记

    for (const segment of segments) {
      if (!avoidDuplicatedSlash || !path.endsWith('/')) path += '/'
      avoidDuplicatedSlash = false

      for (const token of segment) {
        if (token.type === TokenType.Static) {
          path += token.value
        } else if (token.type === TokenType.Param) {
          const { value, repeatable, optional } = token

          // 获取参数值
          const param: string | readonly string[] =
            value in params ? params[value] : ''

          // 校验:非可重复参数不能传数组
          if (isArray(param) && !repeatable) {
            throw new Error(
              `Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
            )
          }

          // 数组参数 → 拼接为 / 分隔的字符串
          const text: string = isArray(param)
            ? (param as string[]).join('/')
            : (param as string)

            // 参数值为空处理
          if (!text) {
            // 可选参数处理
            if (optional) {
              // if we have more than one optional param like /:a?-static we don't need to care about the optional param 
              if (segment.length < 2) {
                // remove the last slash as we could be at the end
                if (path.endsWith('/')) path = path.slice(0, -1) // 移除末尾斜杠
                // do not append a slash on the next iteration
                else avoidDuplicatedSlash = true
              }
            } else throw new Error(`Missing required param "${value}"`)
          }
          path += text // 参数值非空 → 拼接
        }
      }
    }

    // avoid empty path when we have multiple optional params
    // 空路径 → 返回 /;否则返回拼接结果
    return path || '/'
  }

  return {
    re,
    score,
    keys,
    parse,
    stringify,
  }
}

tokenizePath

/**
 * 拆分静态路径段和动态参数段
 * @param path 路由路径字符串(如 /user/:id/profile)
 * @returns 路径令牌数组(如 [['/user/', { name: 'id', regex: '[^/]+' }, '/profile']])
 */
export function tokenizePath(path: string): Array<Token[]> {

  if (!path) return [[]] // 空路径 → 返回 [[]]

  // 根路径 → 返回 [[ROOT_TOKEN]]
  if (path === '/') return [[ROOT_TOKEN]]

  // 非根路径但不以 / 开头 → 抛错(路由路径必须以 / 开头)
  if (!path.startsWith('/')) {
    throw new Error(
      __DEV__
        ? `Route paths should start with a "/": "${path}" should be "/${path}".`
        : `Invalid path "${path}"`
    )
  }

  // if (tokenCache.has(path)) return tokenCache.get(path)!

  function crash(message: string) {
    throw new Error(`ERR (${state})/"${buffer}": ${message}`)
  }

  let state: TokenizerState = TokenizerState.Static  // 初始状态:静态文本
  let previousState: TokenizerState = state // 前一个状态:初始状态为静态文本

  // 最终输出的二维令牌数组
  const tokens: Array<Token[]> = []
  // the segment will always be valid because we get into the initial state
  // with the leading /
  
  let segment!: Token[] // 当前路径段的令牌数组

  // 完成当前路径段的处理(推入tokens,重置segment)
  function finalizeSegment() {
    if (segment) tokens.push(segment)
    segment = []
  }

  // index on the path
  let i = 0
  // char at index
  let char: string = '' // 当前遍历到的字符
  // buffer of the value read
  let buffer: string = '' // 当前路径段的字符缓冲区
  // custom regexp for a param
  let customRe: string = '' // 参数自定义正则缓冲区

  // 令牌生成函数
  function consumeBuffer() {
    if (!buffer) return

    // 静态文本状态处理
    if (state === TokenizerState.Static) {
      segment.push({
        type: TokenType.Static,
        value: buffer,
      })

      // 参数相关状态处理:Param/ParamRegExp/ParamRegExpEnd
    } else if (
      state === TokenizerState.Param ||
      state === TokenizerState.ParamRegExp ||
      state === TokenizerState.ParamRegExpEnd
    ) {
      // 带 */+ 修饰符的可重复参数,必须独占一个路径段(不能和其他令牌共存)
      // 错误示例:路径 /user:ids+
      if (segment.length > 1 && (char === '*' || char === '+'))
        crash(
          `A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`
        )
      segment.push({
        type: TokenType.Param,
        value: buffer,
        regexp: customRe,
        repeatable: char === '*' || char === '+',
        optional: char === '*' || char === '?',
      })
      // 异常状态处理
    } else {
      crash('Invalid state to consume buffer')
    }
    
    buffer = '' // 缓冲区重置
  }

  function addCharToBuffer() {
    buffer += char
  }

  // 状态机核心循环(遍历路径字符)
  while (i < path.length) {
    char = path[i++]

    // 转义字符处理(\):非参数正则状态下,\ 后接的字符直接作为普通字符
    // 示例 1:路径 /user\:id → 用户希望 :id 是静态文本,而非动态参数,需用 \ 转义 :;
    if (char === '\\' && state !== TokenizerState.ParamRegExp) {
      previousState = state // 保存当前状态
      state = TokenizerState.EscapeNext // 切换到「转义下一个字符」状态
      continue
    }

     // 状态机分支处理
    switch (state) {
      case TokenizerState.Static:

        // 检测到路径分隔符 /,表示当前路径段结束,需要收尾当前段的解析
        if (char === '/') {
          // 检查缓冲区是否有内容
          if (buffer) {
            consumeBuffer()
          }
          finalizeSegment()

          // 动态参数解析
        } else if (char === ':') {
          consumeBuffer()
          state = TokenizerState.Param

        } else {
          addCharToBuffer() // 普通字符 → 加入缓冲区
        }
        break

         // 状态2:转义下一个字符
      case TokenizerState.EscapeNext:
        addCharToBuffer()
        state = previousState
        break

         // 状态3:参数名(如 :id 中的 id)
      case TokenizerState.Param:

        // 检测到参数正则开始符 (,表示自定义正则开始
        if (char === '(') {
          state = TokenizerState.ParamRegExp
        } else if (VALID_PARAM_RE.test(char)) {
          addCharToBuffer() // 收集参数名字符
        } else {
          consumeBuffer()
          state = TokenizerState.Static // 恢复静态文本状态
          // go back one character if we were not modifying
          // 非修饰符字符,索引回退一位(让静态状态重新处理该字符)
          if (char !== '*' && char !== '?' && char !== '+') i--
        }
        break

        // 状态4:参数自定义正则(如 :id(\\d+) 中的 \\d+)
      case TokenizerState.ParamRegExp:
        // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
        // it already works by escaping the closing )
        // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
        // is this really something people need since you can also write
        // /prefix_:p()_suffix
        // 处理正则结束符
        if (char === ')') {
          // handle the escaped )
          // 检查最后一个正则字符是否是转义符 \
          if (customRe[customRe.length - 1] == '\\')
            customRe = customRe.slice(0, -1) + char  // 移除转义符 \,将 ) 作为普通字符加入正则

          // 无转义 → 正则结束,切换到 ParamRegExpEnd 状态
          else state = TokenizerState.ParamRegExpEnd
        } else {
          customRe += char // 将字符追加到 customRe 缓冲区
        }
        break

        // 状态5:参数正则结束
      case TokenizerState.ParamRegExpEnd:
        // same as finalizing a param
        consumeBuffer() // 生成参数令牌
        state = TokenizerState.Static
        // go back one character if we were not modifying
        // 索引回退逻辑 → 处理修饰符 / 非法字符
        // 非修饰符(如 //(/a 等):回退索引 → 让静态状态重新处理该字符(避免字符丢失)
        if (char !== '*' && char !== '?' && char !== '+') i--
        customRe = ''
        break

      default:
        crash('Unknown state')
        break
    }
  }

  // 校验:参数正则未闭合(如 :id(\\d+ 缺少 ))
  if (state === TokenizerState.ParamRegExp)
    crash(`Unfinished custom RegExp for param "${buffer}"`)

  consumeBuffer()
  finalizeSegment()

  // tokenCache.set(path, tokens)

  return tokens
}

示例

1、完整的路由配置

const matchers = createRouterMatcher(routes,{});
console.log('matchers',matchers)
console.log("matchers-record", matchers.getRoutes());

image.png

image.png

2、简单路由配置

const matchers = createRouterMatcher(
  [
    {
      path: "/404",
      name: "not-found",
      component: () => import("@/views/404.vue"),
      meta: {
        title: "404 Not Found",
        icon: "404",
      },
    },
  ],
  {}
);
console.log("matchers-record", matchers.getRoutes());

image.png

image.png

3、路径含参数示例

const matchers = createRouterMatcher(
  [
    {
      path: "/lists/:type?",
      name: "lists",
      component: () => import("@/views/lists/ListView.vue"),
      meta: {
        title: "Lists",
        icon: "lists",
        roles: ["admin", "user"],
      },
    },
  ],
  {}
);
console.log("matchers-record", matchers.getRoutes());

image.png

4、含嵌套子路由示例

const matchers = createRouterMatcher(
  [
    {
      path: "/user",
      name: "user",
      component: () => import("@/views/user/UserView.vue"),
      meta: {
        title: "用户管理",
        icon: "user",
        roles: ["admin"],
      },
      children: [
        // 嵌套路由
        {
          path: "lists",
          name: "user-list",
          component: () => import("@/views/user/UserList.vue"),
          beforeEnter: (to, from) => {
            console.log("user-list独享路由", to, from);
            return true;
          },
        },
        {
          path: ":id",
          // name: 'user-detail',
          component: () => import("@/views/user/UserDetail.vue"),
          beforeEnter: (to, from) => {
            console.log("user-detail独享路由", to, from);
            return true;
          },
        },
      ],
      beforeEnter: (to, from) => {
        console.log("user-view独享路由", to, from);
        return true;
      },
    },
  ],
  {}
);
console.log("matchers-record", matchers.getRoutes());

image.png

image.png

image.png