Vue Router
源码分析(二)-- createMatcher
果然还是很在意这个matcher
,这个显眼包在上一篇阅读createRouter
时,时不时在我眼前晃一下。算了咯,再不乐意看也得看看咯。
1. RouterMatcher
照例先看类型接口,这样才晓得这家伙有几斤几两。
先是增删查与解析呗,Router
几个方法addRoute
,removeRoute
,getRoutes
,resolve
都是会调用RouterMatcher
对应的方法。喔哦,这还有个getRecordMatcher
,看签名是通过路由名称来查找matcher
,还挺会玩儿。
typescript
export interface RouterMatcher {
addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
removeRoute: {
(matcher: RouteRecordMatcher): void
(name: RouteRecordName): void
}
getRoutes: () => RouteRecordMatcher[]
// 通过路由名称来查找`matcher`
getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined
/**
* Resolves a location. Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
*
* @param location - MatcherLocationRaw to resolve to a url
* @param currentLocation - MatcherLocation of the current location
*/
resolve: (
location: MatcherLocationRaw,
currentLocation: MatcherLocation
) => MatcherLocation
}
2. createRouterMatcher
这才是今天的主角呀,还好加上注释也才300行,不然我真的会吐。
入参:
routes
:就是初始的路由列表呗;globalOptions
:天晓得这个全局选项是干啥子的嘞。看了眼上一篇,哦,原来是createRouter
的选项参数options
;
接下来嘛,还是熟悉的配方,还是原来的味道。先把类型接口需要的方法都定义出来,然后组装在对象里返回。这套路不能说和createRouter
毫无瓜葛,简直就是一模一样。那就排好队,一个一个来呗。
整了几个常量,虽然不明白为什么弄个matchers
数组的同时,还要来个matcherMap
,但是感觉似乎有点东西,后文应该会有答案吧。
php
/**
* 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<RouteRecordName, RouteRecordMatcher>()
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)
// 省略中间的函数体 ...
// add initial routes
// 添加初始路由
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
下面就是函数体的内容咯。
2.1 getRecordMatcher
getRecordMatcher
就是以路由名称为key
从matcherMap
中取出matcher
。
csharp
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
2.2 addRoute
router.addRoute
只接收两个参数,而matcher.addRoute
却有三个参数,那就是说matcher.addRoute
还和别的函数有一腿咯。呸,海王!而且这个addRoute
内容是真不少啊,那就挑重点了。
-
开局一个常量
isRootAdd
,标记是否是添加在根路由下,后续会根据它来判断是否需要移除路由以防止多余的嵌套; -
将参数
record
处理为标准化的路由记录mainNormalizedRecord
; -
合并全局选项
globalOptions
和当前记录,得到新的选项options
; -
生成数组
normalizedRecords
来处理record
的别名; -
处理
normalizedRecords
各成员的path
,如果某个成员的path
为通配符*
,则会在dev
环境下进行报错提示; -
调用
createRouteRecordMatcher
创建matcher
; -
如果存在原始的记录
originalRecord
,则当前为别名记录,需要放入原始记录的别名数组中; -
否则,
normalizedRecords
中的第一个成员就是原始记录,其余成员为别名; -
遇到顶层有名称的原始记录,则根据记录的名称来移除;
-
递归
mainNormalizedRecord.children
; -
通过
insertMatcher
来实际添加路由,只有matcher.record
同时满足以下两个条件才会被添加:components
中至少有一个component
;- 有
name
或者redirect
csharp
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: (typeof mainNormalizedRecord)[] = [
mainNormalizedRecord,
]
if ('alias' in record) {
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
for (const alias of aliases) {
normalizedRecords.push(
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: originalRecord
? originalRecord.record
: mainNormalizedRecord,
// the aliases are always of the same kind as the original since they
// are defined on the same record
}) as typeof mainNormalizedRecord
)
}
}
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
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.'
)
}
// 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
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))
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]
)
}
}
// 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)
// }
// 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 (
(matcher.record.components &&
Object.keys(matcher.record.components).length) ||
matcher.record.name ||
matcher.record.redirect
) {
insertMatcher(matcher)
}
}
return originalMatcher
? () => {
// since other matchers are aliases, they should be removed by the original matcher
removeRoute(originalMatcher!)
}
: noop
}
2.3 removeRoute
这个就比addRoute
简单多了嘛。接收的参数如果是name
,就用matcherMap
去找,如果是matcher
,就从matchers
里去找下标。
不仅要移除matchers
和matcherMap
中存储的matcher
,还要从二者中递归移除matcher.children
和matcher.alias
中的所有matcher
,这不是犯天条了要株连九族嘛。
scss
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)
}
}
}
2.4 getRoutes
就返回matchers
数组呗。好,原来这么简单,我这就去告诉别人我学会Vue Router
的源码了!
csharp
function getRoutes() {
return matchers
}
2.5 insertMatcher
看过addRoute
的人都知道,addRoute
中调用createRouteRecordMatcher
生成matcher
,而实际将matcher
存起来的是insertMatcher
。
这下就解决开头的疑问了,matchers
数组什么记录都能存,但是matcherMap
只存原始记录。
csharp
function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0
while (
i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// Adding children with empty path should still appear before the parent
// https://github.com/vuejs/router/issues/1124
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++
matchers.splice(i, 0, matcher)
// only add the original record to the name map
// matcherMap只存储原始记录
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
}
2.6 resolve
又是个飞流直下三千尺的函数,这么长我可以不看吗?周深再怎么一往情深,也远不及我此刻眉头皱得深。
存在有效的location.name
时:
- 根据参数
location.name
获取matcher
,获取不到则抛出路由错误,且dev
环境下会根据不合法的loaction.params
来进行告警提示; - 从
location
和currentLocation
中得到路由参数params
,并字符串化为path
;
不存在有效的location.name
但是存在有效的location.path
时:
path
取自location.path
;- 利用
path
从matcher
中拿到params
和name
;
最后,根据parent
属性递归matcher
,将得到的所有matcher
倒序放进matched
数组,使得父级位于前面;matched
数组会与path
、params
、name
等属性在同一个对象中被返回。
javascript
function resolve(
location: Readonly<MatcherLocationRaw>,
currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
let matcher: RouteRecordMatcher | undefined
let params: PathParams = {}
let path: MatcherLocation['path']
let name: MatcherLocation['name']
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
paramsFromLocation(
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 &&
paramsFromLocation(
location.params,
matcher.keys.map(k => k.name)
)
)
// throws if cannot be stringified
path = matcher.stringify(params)
} 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
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)
}
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),
}
}
createMatcher
的函数内容到此就走向了终点,剩下的一个未解之谜是matcher
的创建过程,而这都藏在createRouteRecordMatcher
中。
createRouteRecordMatcher
在观摩createRouteRecordMatcher
之前,最好是先了解一下类型接口RouteRecordMatcher
,知己知彼。类型接口RouteRecordMatcher
继承了PathParser
:
php
export interface PathParser {
/**
* The regexp used to match a url
*/
re: RegExp
/**
* The score of the parser
*/
score: Array<number[]>
/**
* Keys that appeared in the path
*/
keys: PathParserParamKey[]
/**
* Parses a url and returns the matched params or null if it doesn't match. An
* optional param that isn't preset will be an empty string. A repeatable
* param will be an array if there is at least one value.
*
* @param path - url to parse
* @returns a Params object, empty if there are no params. `null` if there is
* no match
*/
parse(path: string): PathParams | null
/**
* Creates a string version of the url
*
* @param params - object of params
* @returns a url
*/
stringify(params: PathParams): string
}
export interface RouteRecordMatcher extends PathParser {
record: RouteRecord
parent: RouteRecordMatcher | undefined
children: RouteRecordMatcher[]
// aliases that must be removed when removing this record
alias: RouteRecordMatcher[]
}
那么现在可以看一看createRouteRecordMatcher
。不看不知道,一看真下头。matcher
的一大部分内容都来自于tokensToParser
创建的parser
,毕竟RouteRecordMatcher
继承了PathParser
。那这部分就不在本文深究了,不然过长了会让人既渴望又害怕。
csharp
export function createRouteRecordMatcher(
record: Readonly<RouteRecord>,
parent: RouteRecordMatcher | undefined,
options?: PathParserOptions
): RouteRecordMatcher {
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
}
今天这个代码就看到这里了,剩下的时间要去陪别的知识咯。