前言
【vue-router源码】系列文章将带你从0开始了解vue-router
的具体实现。该系列文章源码参考vue-router v4.0.15
。
源码地址:https://github.com/vuejs/router
阅读该文章的前提是你最好了解vue-router
的基本使用,如果你没有使用过的话,可通过vue-router官网学习下。
该篇文章将带你理解vue-router
中matcher
的实现。
matcher初识
在开始介绍matcher
实现之前,我们先了解下matcher
是什么?它的作用是什么?
在vue-router
中,每一个我们定义的路由都会被解析成一个对应的matcher
(RouteRecordMatcher
类型),路由的增删改查都会依靠matcher
来实现。
createRouterMatcher
在createRouter
中会通过createRouterMatcher
创建一个matcher
(RouterMatcher
类型)。
export function createRouterMatcher(
routes: RouteRecordRaw[],
globalOptions: PathParserOptions
): RouterMatcher {
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)
function getRecordMatcher(name: RouteRecordName) { // ... }
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// ...
}
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) { // ... }
function getRoutes() { // ... }
function insertMatcher(matcher: RouteRecordMatcher) { // ... }
function resolve(
location: Readonly<MatcherLocationRaw>,
currentLocation: Readonly<MatcherLocation>
): MatcherLocation {
// ...
}
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
createRouterMatcher
接收两个参数:routes
、globalOptions
。其中routes
为我们定义的路由表,也就是在createRouter
时传入的options.routes
,而globalOptions
就是createRouter
中的options
。
createRouterMatcher
中声明了两个变量matchers
、matcherMap
,用来存储通过路由表解析的matcher
(RouteRecordMatcher
类型),然后遍历routes
,对每个元素调用addRoute
方法。最后返回一个对象,该对象有addRoute
、resolve
、removeRoute
、getRoute
、getRecordMatcher
几个属性,这几个属性都对应着一个函数。
接下来我们看下这几个函数:
addRoute
addRoute
函数接收三个参数:record
(新增的路由)、parent
(父matcher
)、originalRecord
(原始matcher
)。
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// used later on to remove by name
const isRootAdd = !originalRecord
// 标准化化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// aliasOf表示此记录是否是另一个记录的别名
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// 声明一个记录的数组用来处理别名
const normalizedRecords: typeof mainNormalizedRecord[] = [
mainNormalizedRecord,
]
// 如果record设置了别名
if ('alias' in record) {
// 别名数组
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
// 遍历别名数组,并根据别名创建记录存储到normalizedRecords中
for (const alias of aliases) {
normalizedRecords.push(
assign({}, mainNormalizedRecord, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
// 如果有原始记录,aliasOf为原始记录,如果没有原始记录就是它自己
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
}) as typeof mainNormalizedRecord
)
}
}
let matcher: RouteRecordMatcher
let originalMatcher: RouteRecordMatcher | undefined
// 遍历normalizedRecords
for (const normalizedRecord of normalizedRecords) {
// 处理normalizedRecord.path为完整的path
const { path } = normalizedRecord
// 如果path不是以/开头,那么说明它不是根路由,需要拼接为完整的path
// { path: '/a', children: [ { path: 'b' } ] } -> { path: '/a', children: [ { path: '/a/b' } ] }
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.'
)
}
// 创建一个路由记录匹配器
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
// 检查是否有丢失的参数
if (__DEV__ && parent && path[0] === '/')
checkMissingParamsInAbsolutePath(matcher, parent)
// 如果有originalRecord,将matcher放入原始记录的alias中,以便后续能够删除
if (originalRecord) {
originalRecord.alias.push(matcher)
// 检查originalRecord与matcher中动态参数是否相同
if (__DEV__) {
checkSameParams(originalRecord, matcher)
}
} else { // 没有originalRecord
// 因为原始记录索引为0,所以originalMatcher为有原始记录所产生的matcher
originalMatcher = originalMatcher || matcher
// 如果matcher不是原始记录产生的matcher,说明此时matcher是由别名记录产生的,此时将matcher放入originalMatcher.alias中
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
// 如果命名并且仅用于顶部记录,则删除路由(避免嵌套调用)
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name)
}
// 遍历children,递归addRoute
if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
originalRecord = originalRecord || matcher
// 添加matcher
insertMatcher(matcher)
}
// 返回一个删除原始matcher的方法
return originalMatcher
? () => {
removeRoute(originalMatcher!)
}
: noop
}
在addRoute
中,会对record
进行标准化处理(normalizeRouteRecord
),如果存在原始的matcher
,也就是originalRecord
,说明此时要添加的路由是另一记录的别名,这时会将originalRecord.record
存入mainNormalizedRecord.aliasOf
中。
const isRootAdd = !originalRecord
// 标准化化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record)
// aliasOf表示此记录是否是另一个记录的别名
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
// 声明一个记录的数组用来处理别名
const normalizedRecords: typeof mainNormalizedRecord[] = [
mainNormalizedRecord,
]
然后会遍历record
的别名,向normalizedRecords
中添加由别名产生的路由:
if ('alias' in record) {
// 别名数组
const aliases =
typeof record.alias === 'string' ? [record.alias] : record.alias!
// 遍历别名数组,并根据别名创建记录存储到normalizedRecords中
for (const alias of aliases) {
normalizedRecords.push(
assign({}, mainNormalizedRecord, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
// 如果有原始记录,aliasOf为原始记录,如果没有原始记录就是它自己
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
}) as typeof mainNormalizedRecord
)
}
}
紧接着会遍历normalizedRecords
:在这个遍历过程中,会首先将path
处理成完整的path
,然后通过createRouteRecordMatcher
方法创建一个matcher
(RouteRecordMatcher
类型),如果matcher
是由别名产生的,那么matcher
会被加入由原始记录产生的matcher
中的alias
属性中。然后会遍历mainNormalizedRecord
的children
属性,递归调用addRoute
方法。在最后,调用insertMatcher
添加新创建的matcher
。
for (const normalizedRecord of normalizedRecords) {
// 处理normalizedRecord.path为完整的path
const { path } = normalizedRecord
// 如果path不是以/开头,那么说明它不是根路由,需要拼接为完整的path
// { path: '/a', children: [ { path: 'b' } ] } -> { path: '/a', children: [ { path: '/a/b' } ] }
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.'
)
}
// 创建一个路由记录匹配器
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
// 检查是否有丢失的参数
if (__DEV__ && parent && path[0] === '/')
checkMissingParamsInAbsolutePath(matcher, parent)
// 如果有originalRecord,将matcher放入原始记录的alias中,以便后续能够删除
if (originalRecord) {
originalRecord.alias.push(matcher)
// 检查originalRecord与matcher中动态参数是否相同
if (__DEV__) {
checkSameParams(originalRecord, matcher)
}
} else { // 没有originalRecord
// 因为原始记录索引为0,所以originalMatcher为有原始记录所产生的matcher
originalMatcher = originalMatcher || matcher
// 如果matcher不是原始记录产生的matcher,说明此时matcher是由别名记录产生的,此时将matcher放入originalMatcher.alias中
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
// 如果存在record.name并且是顶部记录,则删除路由(避免嵌套调用)
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name)
}
// 遍历children,递归addRoute
if ('children' in mainNormalizedRecord) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
// 如果originalRecord是方法传入的,那么originalRecord继续保持
// 如果originalRecord方法未传入。由于原始的matcher总是在索引为0的位置,所以如果有别名,那么这些别名的原始matcher会始终指向索引为0的位置
originalRecord = originalRecord || matcher
// 添加matcher
insertMatcher(matcher)
}
在最后,addRoute
会返回一个删除原始matcher
的方法。
在addRoute
的过程中,会调用createRouteRecordMatcher
方法来创建matcher
,那么matcher
究竟是什么?它是如何被创建的?接下来我们看下createRouteRecordMatcher
的实现。那么在看createRouteRecordMatcher
之前,我们先来了解tokenizePath
、tokensToParser
这两个函数,因为这两个函数是创建matcher
的核心。
tokenizePath的作用是
将path
转为一个token
数组。而tokensToParser
会根据token
数组创建一个路径解析器。这里提到了一个token
的概念,那么什么是token
呢?我们看下vue-router
中token
的类型定义:
token
interface TokenStatic {
type: TokenType.Static
value: string
}
interface TokenParam {
type: TokenType.Param
regexp?: string
value: string
optional: boolean
repeatable: boolean
}
interface TokenGroup {
type: TokenType.Group
value: Exclude<Token, TokenGroup>[]
}
export type Token = TokenStatic | TokenParam | TokenGroup
从其类型中我们可以看出token
分为三种:
-
TokenStatic
:一种静态的token
,说明token
不可变 -
TokenParam
:参数token
,说明token
是个参数 -
TokenGroup
:分组的token
为了更好理解token
,这里我们举几个例子:
-
/one/two/three
对应的token
数组:
[
[{ type: TokenType.Static, value: 'one' }],
[{ type: TokenType.Static, value: 'two' }],
[{ type: TokenType.Static, value: 'three' }]
]
-
/user/:id
对应的token
数组是:
[
[
{
type: TokenType.Static,
value: 'user',
},
],
[
{
type: TokenType.Param,
value: 'id',
regexp: '',
repeatable: false,
optional: false,
}
]
]
-
/:id(\\d+)new
对应的token
数组:
[
[
{
type: TokenType.Param,
value: 'id',
regexp: '\\d+',
repeatable: false,
optional: false,
},
{
type: TokenType.Static,
value: 'new'
}
]
]
从上面几个例子可以看出,token
数组详细描述了path
的每一级路由的组成。例如第3个例子/:id(\\d+)new
,通过token
数组我们能够知道他是一个一级路由(token.lenght = 1
),并且它的这级路由是由两部分组成,其中第一部分是参数部分,第二部分是静态的,并且在参数部分还说明了参数的正则及是否重复、是否可选的配置。
了解了token
是什么,接下来我们看下tokenizePath
是如何将path
转为token
的:
tokenizePath
tokenizePath
的过程就是利用有限状态自动机生成token
数组。
export const enum TokenType {
Static,
Param,
Group,
}
const ROOT_TOKEN: Token = {
type: TokenType.Static,
value: '',
}
export function tokenizePath(path: string): Array<Token[]> {
if (!path) return [[]]
if (path === '/') return [[ROOT_TOKEN]]
// 如果path不是以/开头,抛出错误
if (!path.startsWith('/')) {
throw new Error(
__DEV__
? `Route paths should start with a "/": "${path}" should be "/${path}".`
: `Invalid path "${path}"`
)
}
function crash(message: string) {
throw new Error(`ERR (${state})/"${buffer}": ${message}`)
}
// token所处状态
let state: TokenizerState = TokenizerState.Static
// 前一个状态
let previousState: TokenizerState = state
const tokens: Array<Token[]> = []
// 声明一个片段,该片段最终会被存入tokens中
let segment!: Token[]
// 添加segment至tokens中,同时segment重新变为空数组
function finalizeSegment() {
if (segment) tokens.push(segment)
segment = []
}
let i = 0
let char: string
let buffer: string = ''
// custom regexp for a param
let customRe: string = ''
// 消费buffer,即生成token添加到segment中
function consumeBuffer() {
if (!buffer) return
if (state === TokenizerState.Static) {
segment.push({
type: TokenType.Static,
value: buffer,
})
} else if (
state === TokenizerState.Param ||
state === TokenizerState.ParamRegExp ||
state === TokenizerState.ParamRegExpEnd
) {
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
}
// 遍历path
while (i < path.length) {
char = path[i++]
// path='/\\:'
if (char === '\\' && state !== TokenizerState.ParamRegExp) {
previousState = state
state = TokenizerState.EscapeNext
continue
}
switch (state) {
case TokenizerState.Static:
if (char === '/') {
if (buffer) {
consumeBuffer()
}
// char === /时说明已经遍历完一层路由,这时需要将segment添加到tokens中
finalizeSegment()
} else if (char === ':') { // char为:时,因为此时状态是TokenizerState.Static,所以:后是参数,此时要把state变为TokenizerState.Param
consumeBuffer()
state = TokenizerState.Param
} else { // 其他情况拼接buffer
addCharToBuffer()
}
break
case TokenizerState.EscapeNext:
addCharToBuffer()
state = previousState
break
case TokenizerState.Param:
if (char === '(') { // 碰到(,因为此时state为TokenizerState.Param,说明后面是正则表达式,所以修改state为TokenizerState.ParamRegExp
state = TokenizerState.ParamRegExp
} else if (VALID_PARAM_RE.test(char)) {
addCharToBuffer()
} else { // 例如/:id/one,当遍历到第二个/时,消费buffer,state变为Static,并让i回退,回退后进入Static
consumeBuffer()
state = TokenizerState.Static
if (char !== '*' && char !== '?' && char !== '+') i--
}
break
case TokenizerState.ParamRegExp:
// it already works by escaping the closing )
// TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
// https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
// is this really something people need since you can also write
// /prefix_:p()_suffix
if (char === ')') {
// 如果是\\)的情况,customRe = customRe去掉\\ + char
if (customRe[customRe.length - 1] == '\\')
customRe = customRe.slice(0, -1) + char
else state = TokenizerState.ParamRegExpEnd // 如果不是\\)说明正则表达式已经遍历完
} else {
customRe += char
}
break
case TokenizerState.ParamRegExpEnd: // 正则表达式已经遍历完
// 消费buffer
consumeBuffer()
// 重置state为Static
state = TokenizerState.Static
// 例如/:id(\\d+)new,当遍历到n时,使i回退,下一次进入Static分支中处理
if (char !== '*' && char !== '?' && char !== '+') i--
customRe = ''
break
default:
crash('Unknown state')
break
}
}
// 如果遍历结束后,state还是ParamRegExp状态,说明正则是没有结束的,可能漏了)
if (state === TokenizerState.ParamRegExp)
crash(`Unfinished custom RegExp for param "${buffer}"`)
// 遍历完path,进行最后一次消费buffer
consumeBuffer()
// 将segment放入tokens
finalizeSegment()
// 最后返回tokens
return tokens
}
为了更好理解tokenizePath
的过程。我们以path = '/:id(\\d+)new'
例,我们看一下tokenizePath
的过程:
- 初始状态:
state=TokenizerState.Static; previousState=TokenizerState.Static; tokens=[]; segment; buffer=''; i=0; char=''; customRe='';
- 当
i=0
时,进入TokenizerState.Static
分支,此时char='/'; buffer='';
,不会执行consumeBuffer
,执行finalizeSegment
,该轮结束后发生变化的是segment=[]; i=1; char='/';
- 当
i=1
时,进入TokenizerState.Static
分支,此时char=':'; buffer='';
,执行consumeBuffer
,因为buffer=''
,所以consumeBuffer
中什么都没做,最后state=TokenizerState.Param
,该轮结束后发生变化的是state=TokenizerState.Param; i=2; char=':';
- 当
i=2
时,进入TokenizerState.Param
分支,此时char='i'; buffer='';
,执行addCharToBuffer
,该轮结束后发生变化的是buffer='i'; i=3; char='i';
- 当
i=3
时,过程同4,该轮结束后发生变化的是buffer='id'; i=4; char='d';
- 当
i=4
时,进入TokenizerState.Param
分支,此时char='('; buffer='id';
,此时会将state
变为TokenizerState.ParamRegExp
,说明(
后面是正则,该轮结束后发生变化的是state=TokenizerState.ParamRegExp; i=5; char='(';
- 当
i=5
时,进入TokenizerState.ParamRegExp
分支,此时char='\\'; buffer='id';
,执行customRe+=char
,该轮结束后发生变化的是i=6; char='\\'; customRe='\\'
- 当
i=6
、i=7
时,过程同5,最终发生变化的是i=8; char='+'; customRe='\\d+'
- 当
i=8
时,进入TokenizerState.ParamRegExp
分支,此时char=')'; buffer='id'; customRe='\\d+'
,state
变为TokenizerState.ParamRegExpEnd
,代表正则结束,该轮结束后发生变化的是state=TokenizerState.ParamRegExpEnd; i=9; char=')';
- 当
i=9
时,进入TokenizerState.ParamRegExpEnd
分支,此时char='n'; buffer='id'; customRe='\\d+'
,执行consumeBuffer
,在consumeBuffer
中会向segment
添加一条token
并将buffer
置为空字符串,该token
是{type: TokenType.Param, value: 'id', regexp: '\\d+', repeatable: false, optional: false}
,执行完consumeBuffer
后,state
重置为Static
,customRe
重置为空字符串,i
回退1,该轮结束后发生变化的是segment=[{...}]; state=TokenizerState.Static; buffer=''; customRe=''; char='n';
,注意此时i=9
- 上一轮结束后
i=9
,进入TokenizerState.Static
分支,此时此时char='n'; buffer='';
,执行addCharToBuffer
方法,该轮结束后发生变化的是buffer='n'; i=10; char='n'
- 当
i=10
、i=11
时,过程同11,结束后发生变化的是buffer='new'; i=12; char='w'
- 当
i=12
,结束遍历,执行consumeBuffer
,向segment
添加{type: TokenType.Static, value: 'new'}
一条记录并将buffer
置为空字符串。然后执行finalizeSegment
,将segment
添加到tokens
中,并将segment
置为空数组。最后返回的tokens
如下:
[
[
{
type: TokenType.Param,
value: 'id',
regexp: '\\d+',
repeatable: false,
optional: false,
},
{
type: TokenType.Static,
value: 'new'
}
]
]
状态转移过程图示:
tokenizePath状态转移过程.drawio.png
tokensToParser
tokensToParser
函数接收一个token
数组和一个可选的extraOptions
,在函数中会构造出path
对应的正则表达式、动态参数列表keys
、token
对应的分数(相当于权重,该分数在后续path
的比较中会用到)、一个可以从path
中提取动态参数的函数(parse
)、一个可以根据传入的动态参数生成path
的函数(stringify
),最后将其组成一个对象返回。
const enum PathScore {
_multiplier = 10,
Root = 9 * _multiplier, // 只有一个/时的分数
Segment = 4 * _multiplier, // segment的基础分数
SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
Static = 4 * _multiplier, // type=TokenType.Static时的分数
Dynamic = 2 * _multiplier, // 动态参数分数 /:someId
BonusCustomRegExp = 1 * _multiplier, // 用户自定义正则的分数 /:someId(\\d+)
BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
BonusRepeatable = -2 * _multiplier, // 当正则是可重复时的分数 /:w+ or /:w*
BonusOptional = -0.8 * _multiplier, // 当正则是可选择时的分数 /:w? or /:w*
// these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
BonusStrict = 0.07 * _multiplier, // options.strict: true时的分数
BonusCaseSensitive = 0.025 * _multiplier, // options.strict:true时的分数
}
const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
sensitive: false,
strict: false,
start: true,
end: true,
}
const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g
export function tokensToParser(
segments: Array<Token[]>,
extraOptions?: _PathParserOptions
): PathParser {
const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
// 除了根段“/”之外,分数的数量与segments的长度相同
const score: Array<number[]> = []
// 正则的字符串形式
let pattern = options.start ? '^' : ''
// 保存路由中的动态参数
const keys: PathParserParamKey[] = []
for (const segment of segments) {
// 用一个数组保存token的分数,如果segment.length为0,使用PathScore.Root
const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
// options.strict代表是否禁止尾部/,如果禁止了pattern追加/
if (options.strict && !segment.length) pattern += '/'
// 开始遍历每个token
for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
const token = segment[tokenIndex]
// 当前子片段(单个token)的分数:基础分数+区分大小写 ? PathScore.BonusCaseSensitive : 0
let subSegmentScore: number =
PathScore.Segment +
(options.sensitive ? PathScore.BonusCaseSensitive : 0)
if (token.type === TokenType.Static) {
// 在开始一个新的片段(tokenIndex !== 0)前pattern需要添加/
if (!tokenIndex) pattern += '/'
// 将token.value追加到pattern后。追加前token.value中的.、+、*、?、^、$等字符前面加上\\
// 关于replace,参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/replace
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&')
subSegmentScore += PathScore.Static
} 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
// 用户自定义的正则需要验证正则的正确性
if (re !== BASE_PARAM_PATTERN) {
subSegmentScore += PathScore.BonusCustomRegExp
// 使用前确保正则是正确的
try {
new RegExp(`(${re})`)
} catch (err) {
throw new Error(
`Invalid custom RegExp for param "${value}" (${re}): ` +
(err as Error).message
)
}
}
// /:chapters*
// 如果是重复的,必须注意重复的前导斜杠
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})`
: '/' + subPattern
if (optional) subPattern += '?'
pattern += subPattern
subSegmentScore += PathScore.Dynamic
if (optional) subSegmentScore += PathScore.BonusOptional
if (repeatable) subSegmentScore += PathScore.BonusRepeatable
if (re === '.*') subSegmentScore += PathScore.BonusWildcard
}
segmentScores.push(subSegmentScore)
}
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 += '/?'
if (options.end) pattern += '$'
// allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
else if (options.strict) pattern += '(?:/|$)'
// 根据组装好的pattern创建正则表达式,options.sensitive决定是否区分大小写
const re = new RegExp(pattern, options.sensitive ? '' : 'i')
// 根据path获取动态参数对象
function parse(path: string): PathParams | null {
const match = path.match(re)
const params: PathParams = {}
if (!match) return null
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
}
// 根据传入的动态参数对象,转为对应的path
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 | string[] = value in params ? params[value] : ''
if (Array.isArray(param) && !repeatable)
throw new Error(
`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`
)
const text: string = Array.isArray(param) ? param.join('/') : param
if (!text) {
if (optional) {
// if we have more than one optional param like /:a?-static and there are more segments, we don't need to
// care about the optional param
if (segment.length < 2 && segments.length > 1) {
// 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
}
}
}
return path
}
return {
re,
score,
keys,
parse,
stringify,
}
}
现在我们了解了tokensToParser
和tokenizePath
,然后我们来看createRouteRecordMatcher
的实现:
createRouteRecordMatcher
export function createRouteRecordMatcher(
record: Readonly<RouteRecord>,
parent: RouteRecordMatcher | undefined,
options?: PathParserOptions
): RouteRecordMatcher {
// 生成parser对象
const parser = tokensToParser(tokenizePath(record.path), options)
// 如果有重复的动态参数命名进行提示
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)
}
}
// 将record,parent合并到parser中,同时新增children,alias属性,默认值为空数组
const matcher: RouteRecordMatcher = assign(parser, {
record,
parent,
// these needs to be populated by the parent
children: [],
alias: [],
})
if (parent) {
// 两者都是alias或两者都不是alias
if (!matcher.record.aliasOf === !parent.record.aliasOf)
parent.children.push(matcher)
}
return matcher
}
resolve
resolve
根据传入的location
进行路由匹配,找到对应的matcher
的路由信息。方法接收一个location
和currentLocation
参数,返回一个MatcherLocation
类型的对象,该对象的属性包含:name
、path
、params
、matched
、meta
。
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) { // 如果location存在name属性,可根据name从matcherMap获取matcher
matcher = matcherMap.get(location.name)
if (!matcher)
throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
location,
})
name = matcher.record.name
// 合并location.params和currentLocation中的params
params = assign(
paramsFromLocation(
currentLocation.params,
matcher.keys.filter(k => !k.optional).map(k => k.name)
),
location.params
)
// 如果不能通过params转为path抛出错误
path = matcher.stringify(params)
} else if ('path' in location) { // 如果location存在path属性,根据path从matchers获取对应matcher
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://new-issue.vuejs.org/?repo=vuejs/router.`
)
}
matcher = matchers.find(m => m.re.test(path))
if (matcher) {
// 通过parse函数获取params
params = matcher.parse(path)!
name = matcher.record.name
}
} else { // 如果location中没有name、path属性,就使用currentLocation的name或path获取matcher
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)
}
// 使用一个数组存储匹配到的所有路由
const matched: MatcherLocation['matched'] = []
let parentMatcher: RouteRecordMatcher | undefined = matcher
while (parentMatcher) {
// 父路由始终在数组的开头
matched.unshift(parentMatcher.record)
parentMatcher = parentMatcher.parent
}
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched),
}
}
removeRoute
删除路由。接收一个matcherRef
参数,removeRoute
会将matcherRef
对应的matcher
从matcherMap
和matchers
中删除,并清空matcherRef
对应matcher
的children
与alias
属性。由于matcherRef
对应的matcher
被删除后,其子孙及别名也就没用了,也需要把他们从matcherMap
中和matchers
中删除。
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
// 如果是路由名字:string或symbol
if (isRouteName(matcherRef)) {
const matcher = matcherMap.get(matcherRef)
if (matcher) {
// 删除matcher
matcherMap.delete(matcherRef)
matchers.splice(matchers.indexOf(matcher), 1)
// 清空matcher中的children与alias,
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
获取所有matcher
。
function getRoutes() {
return matchers
}
getRecordMatcher
根据路由名获取对应matcher
。
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
insertMatcher
在添加matcher
时,并不是直接matchers.add
,而是根据matcher.score
进行排序。比较分数时根据数组中的每一项挨个比较,不是比较总分。
function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0
while (
i < matchers.length &&
// matcher与matchers[i]比较,matchers[i]应该在前面
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// matcher的path与matchers[i]不同或matcher不是matchers[i]的孩子
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++
// 插入matcher
matchers.splice(i, 0, matcher)
// 只添加原始matcher到map中
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
}
// 返回0表示a与b相等;返回>0,b先排序;返回<0,a先排序
export function comparePathParserScore(a: PathParser, b: PathParser): 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++
}
return bScore.length - aScore.length
}
function compareScoreArray(a: number[], b: number[]): number {
let i = 0
while (i < a.length && i < b.length) {
const diff = b[i] - a[i]
// 一旦a与b对位索引对应的值有差值,直接返回
if (diff) return diff
i++
}
if (a.length < b.length) {
// 如果a.length为1且第一个值的分数为PathScore.Static + PathScore.Segment,返回-1,表示a先排序,否则返回1,表示b先排序
return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
? -1
: 1
} else if (a.length > b.length) {
// 如果b.length为1且第一个值的分数为PathScore.Static + PathScore.Segment,返回-1,表示b先排序,否则返回1,表示a先排序
return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
? 1
: -1
}
return 0
}
假设matcherA
是需要添加的,matchers
中此时只有一个matcherB
,matcherA.score=[[1, 2]]
,matcherB.score=[[1,3]]
,那么matcherA
是怎么添加到matchers
中的呢?过程如下:
- 初始化
matchers
索引i=0
- 首先比较
matcherA.score[0][0]
与matcherB.score[0][0]
,matcherB.score[0][0]-matcherA.score[0][0] === 0
继续比较 -
matcherA.score[0][1]
与matcherB.score[0][1]
,因为matcherB.score[0][1]-matcherA.score[0][1] > 0
,i++
-
i=1
时,由于i=matchers.length
,结束循环 - 执行
matchers.splice(i, 0, matcher)
,此时i=1
,所以matcherA
会被添加到索引为1的位置
如果matcherA.score=[[1,3,4]]
呢? 在比较时因为前两个索引对应的值都是一样的,这时会进入compareScoreArray
的以下分支:
if (a.length > b.length) {
return b.length === 1 && b[0] === PathScore.Static + PathScore.Segment
? 1
: -1
}
以上结果返回-1,matcherA
会被添加到索引为0的位置。
如果matcherA.score=[[1]]
,进入compareScoreArray
的以下分支:
if (a.length < b.length) {
return a.length === 1 && a[0] === PathScore.Static + PathScore.Segment
? -1
: 1
}
因为matcherA.score[0].length === 1
,这时就需要考虑token
的类型里,假设token
是个Static
类型的,那么返回-1,matcherA
添加到索引为0的位置。如果token
不是Static
类型的,返回1,matcherA
添加到索引为1的位置。
所以insertMatcher
,会将权重高的matcher
放在matchers
前面;matcherMap
中只存放原始matcher
。
总结
经过上面分析,我们知道了matcher
是什么,如何实现的
在vue-router
通过matcher
完成路由的匹配、增删改查等操作,其中会使用matchers
和matcherMap
来存储matcher
。matchers
中权重(分数)高的matcher
优先;matcherMap
中的key
是注册路由时路由表的name
,只存放原始matcher
。
matcher
中包含了路由path
对应的正则re
、路由的分数score
、动态参数列表keys
、可从path
中提取动态参数的parse(path)
函数、可传入参数对象将其转为对应path
的stringify(param)
函数、父matcher
(parent
)、路由的标准化版本record
、子matcher
(children
)、由别名产生的matcher
(alias
)
export interface PathParser {
re: RegExp
score: Array<number[]>
keys: PathParserParamKey[]
parse(path: string): PathParams | null
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[]
}
在生成matcher
的过程中会将path
转换成token
数组(二维数组,第一维度中每个维度代表一级路由,第二维度中每个维度代表路由的组成),路由正则的生成、动态参数的提取、分数的计算、stringify
全都依托这个token
数组实现。
网友评论