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
}
内部函数 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
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());
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());
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());
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());