这是我参与更文挑战的第8天,活动详情查看更文挑战
本文将接着之前分析vue-router4的routers属性和安装VueRouter实例
routes 属性
回到createRouter方法中,可以看到该方法中只有一个地方用到了options.routes,它作为createRouterMatcher参数,执行后返回一个RouterMatcher类型的对象
export function createRouter(options: RouterOptions): Router {
const matcher = createRouterMatcher(options.routes, options)
// ....
}
复制代码
matcher模块的入口是src/matcher/index.ts,该模块提供了路由配置相关的属性和方法,matcher接口定义如下
// src/matcher/index.ts
interface RouterMatcher {
addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
removeRoute: {
(matcher: RouteRecordMatcher): void
(name: RouteRecordName): void
}
getRoutes: () => RouteRecordMatcher[]
getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined
resolve: (
location: MatcherLocationRaw,
currentLocation: MatcherLocation
) => MatcherLocation
}
复制代码
createRouterMatcher函数的基本逻辑简化后的代码如下
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 {
// ...
}
// add initial routes
routes.forEach(route => addRoute(route))
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
}
复制代码
该函数接收两个参数,第一个参数是路由配置数组,第二个参数是VueRouter初始化时传进来的options。然后声明两个变量matchers和matcherMap,然后是声明一系列方法,在return之前,遍历routes,通过addRoute方法,将路由配置转化为matcher
现在来逐个分析这几个方法
- addRoute方法
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
let isRootAdd = !originalRecord
let mainNormalizedRecord = normalizeRouteRecord(record)
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
const options: PathParserOptions = mergeOptions(globalOptions, record)
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, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
}) as typeof mainNormalizedRecord
)
}
}
let matcher: RouteRecordMatcher
let originalMatcher: RouteRecordMatcher | undefined
for (const normalizedRecord of normalizedRecords) {
let { path } = normalizedRecord
if (parent && path[0] !== '/') {
let parentPath = parent.record.path
let connectingSlash =
parentPath[parentPath.length - 1] === '/' ? '' : '/'
normalizedRecord.path =
parent.record.path + (path && connectingSlash + path)
}
matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
if (originalRecord) {
originalRecord.alias.push(matcher)
} else {
originalMatcher = originalMatcher || matcher
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name)
}
if ('children' in mainNormalizedRecord) {
let children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
originalRecord = originalRecord || matcher
insertMatcher(matcher)
}
return originalMatcher
? () => {
removeRoute(originalMatcher!)
}
: noop
}
复制代码
这个方法要做的事情就是,根据路由配置对象,创建一个matcher对象,然后添加到matchers数组中,最后根据originalMatcher条件return一个移除路由方法或者noop(let noop = () => {})。应用中传进来的路由配置是不完整的,所以需要首先通过normalizeRouteRecord方法对路由配置格式化,生成完整的路由配置对象,其中props属性通过normalizeRecordProps函数格式化,格式化对象是根据路由配置对象中的component或者components生成,如果存在component属性,则props对象包含一个default属性,并赋值为配置中的props,否则取components对象中的key,并从路由配置属性props中取对应的值
function normalizeRouteRecord(
record: RouteRecordRaw
): RouteRecordNormalized {
return {
path: record.path,
redirect: record.redirect,
name: record.name,
meta: record.meta || {},
aliasOf: undefined,
beforeEnter: record.beforeEnter,
props: normalizeRecordProps(record),
children: record.children || [],
instances: {},
leaveGuards: new Set(),
updateGuards: new Set(),
enterCallbacks: {},
components:
'components' in record
? record.components || {}
: { default: record.component! },
}
}
function normalizeRecordProps(
record: RouteRecordRaw
): Record<string, _RouteRecordProps> {
const propsObject = {} as Record<string, _RouteRecordProps>
const props = (record as any).props || false
if ('component' in record) {
propsObject.default = props
} else {
for (let name in record.components)
propsObject[name] = typeof props === 'boolean' ? props : props[name]
}
return propsObject
}
复制代码
调用normalizeRouteRecord方法格式化路由配置对象后,将处理后的mainNormalizedRecord对象添加到normalizedRecords数组中
接着判断’alias’ in record,如果有别名,则添加记录到normalizedRecords数组,基本逻辑是复制mainNormalizedRecord,然后重新设置components,path,aliasOf属性,换句话说,别名的实现原理是通过拷贝记录,并调整部分属性得到新的记录
以上代码都是为接下来创建matcher做准备,继续往下分析代码,首先准备两个变量:matcher、originalMatcher,然后开始遍历normalizedRecords
在遍历的内部,根据路由配置对象,创建matcher,并插入到matchers中,分为以下几步:
-
1、如果是子路由配置,且path不是以/开头,则将父路由的path和子路由的path加起来生成完整path
-
2、调用createRouteRecordMatcher方法,创建matcher对象,如果存在parent,则在parent.children中添加当前matcher对象
function createRouteRecordMatcher(
record: Readonly<RouteRecord>,
parent: RouteRecordMatcher | undefined,
options?: PathParserOptions
): RouteRecordMatcher {
const parser = tokensToParser(tokenizePath(record.path), options)
const matcher: RouteRecordMatcher = assign(parser, {
record,
parent,
children: [],
alias: [],
})
if (parent) {
if (!matcher.record.aliasOf === !parent.record.aliasOf)
parent.children.push(matcher)
}
return matcher
}
复制代码
matcher对象类型为RouteRecordMatcher,它继承自PathParser接口,所以一个matcher对象应该包含以下属性和方法,前面五个属性或方法是通过tokensToParser(tokenizePath(record.path), options)创建的,下面将在该方法中分析这几个属性或方法的实现逻辑
-re: RegExp
- score: Array<number[]>
- keys: PathParserParamKey[]
- parse(path: string): PathParams | null
- stringify(params: PathParams): string
- record: RouteRecord 保存格式化后的路由配置记录
- parent: RouteRecordMatcher | undefined 保存父路由matcher对象
- children: RouteRecordMatcher[] 子路由,初始化为空数组
- alias: RouteRecordMatcher[] 别名,初始化为空数组
在分析tokensToParser之前,需要先看一下tokenizePath(record.path),该方法将path转化为一个token数组。
export const enum TokenType {
Static,
Param,
Group,
}
const enum TokenizerState {
Static,
Param,
ParamRegExp, // custom re for a param
ParamRegExpEnd, // check if there is any ? + *
EscapeNext,
}
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
const ROOT_TOKEN: Token = {
type: TokenType.Static,
value: '',
}
const VALID_PARAM_RE = /[a-zA-Z0-9_]/
// After some profiling, the cache seems to be unnecessary because tokenizePath
// (the slowest part of adding a route) is very fast
// const tokenCache = new Map<string, Token[][]>()
export function tokenizePath(path: string): Array<Token[]> {
if (!path) return [[]]
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[]
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,
})
} 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
}
while (i < path.length) {
char = path[i++]
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
case TokenizerState.EscapeNext:
addCharToBuffer()
state = previousState
break
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
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
else state = TokenizerState.ParamRegExpEnd
} else {
customRe += char
}
break
case TokenizerState.ParamRegExpEnd:
// same as finalizing a param
consumeBuffer()
state = TokenizerState.Static
// go back one character if we were not modifying
if (char !== '*' && char !== '?' && char !== '+') i--
customRe = ''
break
default:
crash('Unknown state')
break
}
}
if (state === TokenizerState.ParamRegExp)
crash(`Unfinished custom RegExp for param "${buffer}"`)
consumeBuffer()
finalizeSegment()
// tokenCache.set(path, tokens)
return tokens
}
复制代码
这个函数的目的是将path字符串转换为数组,方便后续的处理。
比如/user会转换为[[{type: 0, value: 'user'}]]
, /user/:id则会转换成:
[
[{type: 0, value: "user"}],
[{type: 1, value: "id", regexp: "", repeatable: false, optional: false}],
]
复制代码
再回到tokensToParser函数进行分析PathParser是如何生成的
- re
就是一个正则表达式,通过从参数传进来的path的tokens以及一些列的条件判断,将token转换为匹配path的正则表达式
const BASE_PATH_PARSER_OPTIONS: Required<_PathParserOptions> = {
sensitive: false,
strict: false,
start: true,
end: true,
}
function tokensToParser(
segments: Array<Token[]>,
extraOptions?: _PathParserOptions
): PathParser {
const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions)
let pattern = options.start ? '^' : ''
for (const segment of segments) {
// 遍历tokens逐步完善正则表达式
// TODO:这里挖个坑,以后再分析如何生成正则表达式的
}
if (!options.strict) pattern += '/?'
if (options.end) pattern += '$'
else if (options.strict) pattern += '(?:/|$)'
const re = new RegExp(pattern, options.sensitive ? '' : 'i')
// ...
}
复制代码
- score
给当前path计算一个分数,在后续path之间进行比较时,可以用到score的值,相当于权重的比较
let score: Array<number[]> = []
for (const segment of segments) {
const segmentScores: number[] = segment.length ? [] : [PathScore.Root]
// ...
score.push(segmentScores)
}
if (options.strict && options.end) {
const i = score.length - 1
score[i][score[i].length - 1] += PathScore.BonusStrict
}
复制代码
- keys
保存路由中的动态参数
const keys: PathParserParamKey[] = []
for (const segment of segments) {
// ...
if (token.type === TokenType.Param) {
const { value, repeatable, optional, regexp } = token
keys.push({
name: value,
repeatable,
optional,
})
}
// ...
}
复制代码
- parse
传入path参数,然后根据re,然后遍历得到的结果,获取动态参数对象
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
}
复制代码
- stringify
该方法传入params对象,然后返回参数对象结合path组成的替换了参数值的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 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
}
}
}
return path
}
复制代码
经过以上复杂的步骤,就得到了一个完整的matcher对象,实属不易
-
3、然后处理originalMatcher属性,如果是首次赋值,则将originalMatcher赋值为matcher,后面的遍历中,不再重新赋值,而是将matcher添加到originalRecord.alias数组中
-
4、接着根据’children’ in mainNormalizedRecord条件判断是否有子路由,如果有子路由,则遍历mainNormalizedRecord.children数组,并调用addRoute方法,参数为:children[i], matcher, originalRecord && originalRecord.children[i]
-
5、最后调用insertMatcher(matcher)方法,将matcher添加到matchers中,并更新matcherMap
function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0
while (
i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0
)
i++
matchers.splice(i, 0, matcher)
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
}
复制代码
addRoute方法执行完毕
- resolve方法
resolve方法返回MatcherLocation对象,该对象包含的属性为:name、path、params、matched、meta,作用就是根据传入的location进行路由匹配,找到location对应的matcher对应的路由信息
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,
})
name = matcher.record.name
params = assign(
paramsFromLocation(
currentLocation.params,
matcher.keys.filter(k => !k.optional).map(k => k.name)
),
location.params
)
path = matcher.stringify(params)
} else if ('path' in location) {
path = location.path
matcher = matchers.find(m => m.re.test(path))
if (matcher) {
params = matcher.parse(path)!
name = matcher.record.name
}
} else {
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,参数类型可以传入路由name属性或者matcher对象,然后通过matcherRef找到对应的matcher或者matcher索引,在matcherMap、matchers中删除对应的matcher,然后再递归删除matcher.children和matcher.alias中对该matcher对象的引用
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 {
let 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方法
该方法直接返回matchers数组
function getRoutes() {
return matchers
}
复制代码
- getRecordMatcher方法
该方法十分简单,就是通过路由名称从matcherMap中获取对应的matcher对象
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
复制代码
安装VueRouter实例
通过上面的分析,我们了解到:
通过createRouter(options)方法创建VueRouter对象
该方法的参数为一个配置对象,该对象必须提供两个属性:history和routes
history根据需要可以通过VueRouter提供的三种方法创建不同类型的history对象,该对象提供了路由属性和路由跳转方法
routes为路由配置,VueRouter根据routes配置创建了matcher对象,通过matcher对象,可以为VueRouter实例提供了诸如添加路由、匹配路由、移除路由等路由配置相关的属性和方法
接下来我们继续分析创建了VueRouter实例对象后,如何将VueRouter实例和Vue实例关联起来的
在文档示例代码中,通过 app.use 将VueRouter实例对象添加到Vue实例对象中
// 5. 创建并挂载根实例
const app = Vue.createApp({})
//确保 _use_ 路由实例使整个应用支持路由。
app.use(router)
复制代码
app.use(router)执行时,实际上调用了VueRouter实例的install方法
export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = {
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
}
export function createRouter(options: RouterOptions): Router {
// shallowRef: 创建一个 ref,它跟踪自己的 .value 更改,但不会使其值成为响应式的。
const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
START_LOCATION_NORMALIZED
)
let routerHistory = options.history
const router: Router = {
install(app: App) {
const router = this
// 在vue实例中,注册全局路由组件 RouterLink 和 RouterView
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
// 在vue实例给 config.globalProperties.$router 赋值为当前 VueRouter 实例
app.config.globalProperties.$router = router
/**
* 当读取 app.config.globalProperties.$route 时,
* 返回 unref(currentRoute), 即当前路由信息,初始值为path为`/`的对象
*/
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
})
if (
isBrowser &&
// 避免在多个应用中使用router时多次 push,只有在首次install时stated才是false
!started &&
currentRoute.value === START_LOCATION_NORMALIZED
) {
started = true
// 跳转到浏览器url中对应的路由
push(routerHistory.location)
}
// 复制currentRoute对象并转为响应式对象reactiveRoute,该对象可以在组件中通过inject routeLocationKey 获取
const reactiveRoute = {} as {
[k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
RouteLocationNormalizedLoaded[k]
>
}
for (let key in START_LOCATION_NORMALIZED) {
// @ts-ignore: the key matches
reactiveRoute[key] = computed(() => currentRoute.value[key])
}
// 向vue实例中注入 router 相关 provider,组件使用 inject 来接收这几个值
// 这几个值的属性都是 Symbol 类型,可以通过 src/injectionSymbols.ts 源码了解
app.provide(routerKey, router)
app.provide(routeLocationKey, reactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
// 拦截vue实例unmount方法,当vue实例卸载时,重置某些属性和事件解绑,然后再执行vue实例卸载方法
let unmountApp = app.unmount
installedApps.add(app)
app.unmount = function () {
installedApps.delete(app)
if (installedApps.size < 1) {
removeHistoryListener()
currentRoute.value = START_LOCATION_NORMALIZED
started = false
ready = false
}
unmountApp()
}
},
}
}
复制代码
总的来说,install方法做了以下几件事:
- 注册两个路由组件为vue全局组件
- 在app.config.globalProperties上添加route属性,route为当前location对应的路由对象
- 如果是首次install,则通过push方法跳转到url对应的路由
- 注入三个VueRouter相关的provider
- 拦截vue实例的unmount方法,在unmount方法调用之前,先执行VueRouter相关的卸载工作
总结
在阅读源码的过程中,Typescript使阅读源码相对少吃点力,通过类型定义对理解每个变量的作用提供了很好的帮助。从VueRouter实例对象的创建开始分析,对路由的基本实现原理有了一定的理解,也认识到要实现一个功能完善的路由功能,不是一件很简单的事情,需要考虑的边界问题很多。由于能力有限,也还存在很多细节不能够理解。阅读源码,始终是让人受益匪浅的一件事,不管目前能够理解多少。