前言
【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
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' } ] ]
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
数组实现。