Matcher
createRouterMatcher是vue-router中的一个重要函数,它用于创建路由匹配器(route matcher)。
类型
export interface RouterMatcher {
addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
removeRoute: {
(matcher: RouteRecordMatcher): void
(name: RouteRecordName): void
}
getRoutes: () => RouteRecordMatcher[]
getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined
/**
* @param location - MatcherLocationRaw to resolve to a url
* @param currentLocation - MatcherLocation of the current location
*/
resolve: (
location: MatcherLocationRaw,
currentLocation: MatcherLocation
) => MatcherLocation
}
主要作用
其主要作用是:
- 接收 routes 配置和 options 参数
- 调用 addRoute 递归处理配置,生成路由记录的匹配器 matcher
- 保存命名路由与 matcher 映射在 matcherMap 中
- 按得分排序生成 matcher 数组 matchers
- 创建路由解析方法 resolve
- 返回一个对象,包含以下方法:
- addRoute: 添加路由记录
- resolve: 解析路由位置
- removeRoute: 删除路由记录
- getRoutes: 获取路由记录列表
- getRecordMatcher: 获取单个路由记录的匹配器
- matcher 对象仅在 createRouter 内部被创建和使用
- matcher 提供了后续路由匹配与解析的能力
- router 实例内部调用 matcher 的逻辑主要通过闭包实现,并没有通过实例属性访问
matchers
这是一个 RouteRecordMatcher 数组,存储经过处理的路由记录匹配器。 通过调用 addRoute 方法递归处理路由配置后,生成的匹配器会插入这个数组,并按得分排序。 这使得后续可以通过数组顺序搜索快速匹配路由。
matcherMap
这是一个 Map 对象, key 是路由名称,value 是对应的 RouteRecordMatcher。 在添加拥有 name 的路由记录时,会同时保存到这个 Map 中。 这使得后续可以直接通过路由名称快速获取对应的匹配器。
所以:
- matchers 数组支持通过顺序搜索快速匹配路由路径。
- matcherMap 通过名称索引路由记录,用于快速获取命名路由的匹配器。
两者共同组成了路由匹配的索引系统,使得路由解析可以快速高效地完成。 它们充分利用了添加路由阶段构建的路由匹配元数据,为后续的匹配与导航提供了基础数据支持。
insertMatcher()
insertMatcher是createRouterMatcher中的一个辅助方法,主要作用是将生成的 RouteRecordMatcher 插入到 matchers 数组中。
主要逻辑:
- 定义插入位置索引 i,初始为 0
- 遍历 matchers,如果新 matcher 的得分更高,则 i++
- 在 i 位置插入新 matcher
- 如果路由记录有名称且不是别名记录,则将名称和 matcher 映射保存到 matcherMap 中
可以看出,insertMatcher 主要完成了以下工作:
- 根据 matcher 的优先级计算插入位置
- 将 matcher 按顺序插入 matchers 数组中
- 维护 matcherMap 的映射关系
这样可以保证:
- matchers 中的 matcher 顺序按得分排序
- matcherMap 只包含原始记录的映射
insertMatcher 被 addRoute 在生成 matcher 后调用,按顺序将新 matcher 插入已有序列中。
它维护了 matchers 的顺序性,也通过 matcherMap 维护了命名路由的快速映射。 insertMatcher 是维护路由匹配元数据的重要辅助方法,使得后续的路由匹配可以高效进行。
addRoute()
addRoute是createRouterMatcher中的一个重要方法,主要用于处理路由配置,生成对应的路由记录(RouteRecord)以及路由匹配器( RouteRecordMatcher)。
主要逻辑:
- 标准化路由记录
- 生成对应的matcher
- 处理嵌套子路由
- 保存命名记录与matcher的映射
- 插入matcher到有序数组
详细逻辑:
- 接收原始路由记录record,父匹配器parent和原始匹配器originalRecord
- 使用normalizeRouteRecord标准化record
- 处理record的alias生成normalizedRecords
- 遍历normalizedRecords生成matcher
- 如果有parent,拼接子路由的路径
- 使用createRouteRecordMatcher生成matcher
- 如果是alias,关联到originalMatcher
- 如果是顶级添加,保存映射到matcherMap
- 递归处理子路由记录
- 将非空matcher插入matchers数组
- 返回移除路由的函数
源码如下:
/**
* 添加路由记录
* @param record 路由配置
* @param parent 父级路由匹配器
* @param originalRecord 原始匹配器
*/
function addRoute(record: RouteRecordRaw, parent?: RouteRecordMatcher, originalRecord?: RouteRecordMatcher) {
const isRootAdd = !originalRecord
// 调用 normalizeRouteRecord 标准化路由配置
const mainNormalizedRecord = normalizeRouteRecord(record)
if (__DEV__) {
checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent)
}
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
const normalizedRecords: (typeof mainNormalizedRecord)[] = [mainNormalizedRecord]
// 处理 record 的 alias 生成 normalizedRecords
if ('alias' in record) {
const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias!
for (const alias of aliases) {
normalizedRecords.push(assign({}, mainNormalizedRecord, {
components: originalRecord ? originalRecord.record.components : mainNormalizedRecord.components,
path: alias,
aliasOf: originalRecord ? originalRecord.record : mainNormalizedRecord,
}) as typeof mainNormalizedRecord
)
}
}
let matcher: RouteRecordMatcher
let originalMatcher: RouteRecordMatcher | undefined
// 遍历 normalizedRecords 生成 matcher
for (const normalizedRecord of normalizedRecords) {
const { path } = normalizedRecord
// 如果有 parent, 拼接子路由的路径
if (parent && path[0] !== '/') {
const parentPath = parent.record.path
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://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.')
}
// 使用 createRouteRecordMatcher 生成 matcher
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
if (__DEV__ && parent && path[0] === '/')
checkMissingParamsInAbsolutePath(matcher, parent)
if (originalRecord) {
originalRecord.alias.push(matcher)
if (__DEV__) {
checkSameParams(originalRecord, matcher)
}
} else {
originalMatcher = originalMatcher || matcher
if (originalMatcher !== matcher)
originalMatcher.alias.push(matcher)
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name)
}
// 递归处理子路由记录
if (mainNormalizedRecord.children) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(children[i], matcher, originalRecord && originalRecord.children[i])
}
}
originalRecord = originalRecord || matcher
if ((matcher.record.components && Object.keys(matcher.record.components).length) || matcher.record.name || matcher.record.redirect) {
insertMatcher(matcher)
}
}
// 返回移除路由的函数
return originalMatcher ? () => {
removeRoute(originalMatcher!)
} : noop
}
resolve()
resolve方法是createRouterMatcher中非常重要的路由解析逻辑
主要逻辑:
- 尝试从name、path、currentLocation匹配位置信息
- 解析出对应matcher
- 生成完整的匹配链表
- 返回包含matcher及参数信息的location对象
详细逻辑:
- 接收原始的location和当前位置currentLocation
- 如果location有name,从matcherMap获取对应matcher
- 合并参数,生成路径path
- 如果只有path,用matchers匹配路径获取matcher
- 如果仍未匹配则抛出MATCHER_NOT_FOUND的错误
- 从matcher向上递归生成matched匹配链表
- 返回解析后的location对象
源码如下:
/**
* 路由解析的核心方法
* @param location 原始的location
* @param currentLocation 当前位置
*/
function resolve(location: Readonly<MatcherLocationRaw>, currentLocation: Readonly<MatcherLocation>): MatcherLocation {
let matcher: RouteRecordMatcher | undefined
let params: PathParams = {}
let path: MatcherLocation['path']
let name: MatcherLocation['name']
// 如果location有name, 从matcherMap获取对应matcher
if ('name' in location && location.name) {
matcher = matcherMap.get(location.name)
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, { location, })
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(currentLocation.params, matcher.keys.filter(k => !k.optional).map(k => k.name)), location.params && paramsFromLocation(location.params, matcher.keys.map(k => k.name)))
// 生成路径path
path = matcher.stringify(params)
} else if ('path' in location) {
// 没有name,有path
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.`)
}
// 用matchers匹配路径获取matcher
matcher = matchers.find(m => m.re.test(path))
if (matcher) {
params = matcher.parse(path)!
name = matcher.record.name
}
} else {
// 如果两者都没有,以currentLocation匹配
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
params = assign({}, currentLocation.params, location.params)
path = matcher.stringify(params)
}
// 从matcher向上递归生成matched匹配链表
const matched: MatcherLocation['matched'] = []
let parentMatcher: RouteRecordMatcher | undefined = matcher
while (parentMatcher) {
matched.unshift(parentMatcher.record)
parentMatcher = parentMatcher.parent
}
// 返回解析后的location对象
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
}
}
removeRoute()
removeRoute方法是createRouterMatcher中的路由删除逻辑,它可以用于删除已添加的路由记录。
主要逻辑:
- 根据名称或匹配器引用删除
- 从matchers数组和matcherMap中删除
- 递归删除相关联的匹配器
详细逻辑:
- 判断输入是路由名称还是路由记录匹配器
- 如果是名称,从matcherMap中取出对应匹配器
- 从matchers数组中删除匹配器
- 如果有名称,从matcherMap中删除键值对
- 递归删除匹配器的子匹配器和别名匹配器
这样就可以完整删除之前addRoute添加的路由记录了。removeRoute与addRoute相反,在router动态删除路由时调用,用于在matcher内部同步删除不再使用的路由匹配器。 它会递归删除所有相关联的匹配器,保证matcher内部状态的精确一致。
源码如下:
/**
* 删除已添加的路由记录
* @param matcherRef
*/
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
// 根据名称删除
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)
}
} 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)
}
}
}
getRoutes()
getRoutes是createRouterMatcher中的一个简单方法,用于获取所有已添加的路由记录匹配器。
getRoutes方法的实现很简单:
function getRoutes() {
return matchers
}
它直接返回了内部存储匹配器的matchers数组。 这样使用getRoutes就可以获取到所有的路由匹配器集合。
getRoutes的作用主要有:
- 插件可以通过该方法获取所有的路由配置
- 可以在开发环境用于输出路由状态做调试
- 在服务端渲染时,可以获取路由状态串行化
- 可以通过返回的匹配器重新调用match和resolve
所以 getRoutes 为外部提供了获取路由内部状态的途径。它将内部的 matchers 数组直接返回,暴露出所有的路由匹配器。
这可以帮助开发调试或者重新处理路由匹配。是 createRouterMatcher 对外提供状态的一个简单访问点。
getRecordMatcher()
getRecordMatcher是createRouterMatcher中获取单个路由记录匹配器的方法。
其定义为:
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
它接受一个路由名称,从内部的matcherMap中获取对应记录的匹配器。 matcherMap在添加拥有name的路由记录时,会一并保存路由名称和匹配器的映射。
getRecordMatcher利用了这个索引表,可以直接通过名称查找对应路由的匹配器。 相比getRoutes返回所有的匹配器, getRecordMatcher只获取单一匹配器。
它提供了一种根据路由名称获取匹配器的途径。
主要用于:
- 通过路由名称直接获取对应路由状态
- 在 NavigationGuard 中匹配名称获取路由进行校验
- 在服务端渲染时,只获取某一路由的匹配器
所以getRecordMatcher通过路由名称访问匹配器,是一种更精细的路由状态获取方式。它基于matcherMap提供了高效的命名路由匹配器查询。