本文为原创文章,引用请注明出处,欢迎大家收藏和分享💐💐
感谢大家继续阅读《Vue Router 4 源码探索系列》专栏,你可以在下面找到往期文章: 《vue router 4 源码篇:路由诞生——createRouter原理探索》 《vue router 4 源码篇:路由matcher的前世今生》 《vue router 4 源码篇:router history的原生结合》 《vue router 4 源码篇:导航守卫该如何设计(一)》
哈喽大咖好,我是跑手,本次给大家继续讲解下vue-router@4.x
中router matcher
的实现。
在上节讲到,createRouter
方法的第一步就是根据传进来的路由配置列表,为每项创建matcher。这里的matcher可以理解成一个路由页面的匹配器,包含了路由常规方法。而创建matcher,调用了createRouterMatcher
方法。
createRouterMatcher
执行完后,会返回的5个函数{ addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
,为后续的路由创建提供帮助。这些函数的作用,无非就是围绕着上面说到的matcher
增删改查操作,例如,getRoutes
用于返回所有matcher,removeRoute
则是删除某个指定的matcher。。。
为了方便大家阅读,我们先看下创建的matcher最终长啥样?我们可以使用getRoutes()
方法获取到的对象集,得到最终生成的matcher列表:
import {
createRouterMatcher,
createWebHistory,
} from 'vue-router'
export const routerHistory = createWebHistory()
const options = {
// your options...
}
console.log('matchers:', createRouterMatcher(options.routes, options).getRoutes())
输出:
其中,record
字段就是我们经常使用到的vue-router
路由对象(即router.getRoute()
得到的对象),这样理解方便多了吧 \手动狗头。。。
接下来,我们分别对**addRoute, resolve, removeRoute, getRoutes, getRecordMatcher
**这5个方法解读,全面了解**vue router
**是如何创建matcher的。
讲了一大堆,还是回归到源码。createRouterMatcher
函数一共286行,初始化matcher入口在代码340行,调用的方法是addRoute
。
record
(需要处理的路由)、parent
(父matcher
)、originalRecord
(原始matcher
),其中后两个是可选项,意思就是只传record则会认为是一个简单路由「无父无别名」并对其处理,假如带上第2、3参数,则还要结合父路由或者别名路由处理扩展阅读:别名路由
// 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,
]
在执行过程中,先对record
调用normalizeRouteRecord
进行标准化处理,再调用mergeOptions
方法把自身options与全局options合并得到最终options,然后把结果放进normalizedRecords
数组存储。
再讲解下normalizedRecords
,它是一个存储标准化matcher的数组,数组每一项都包含是matcher所有信息:options、parent、compoment、alias等等。。。在接下来要对matcher进行完成初始化的流程中,只要遍历这个数组就行了。
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
)
}
}
然后就是处理别名路由,如果record
设置了别名,则把原record
(也就是传进来的第三个参数),当然这些信息也要塞进normalizedRecords
数组保存,以便后续对原record处理。
扩展阅读:vue router alias
万事俱备,接下来就要遍历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)
首先,生成普通路由和嵌套路由的path,然后调用createRouteRecordMatcher 方法生成一个路由匹配器,至于createRouteRecordMatcher
内部逻辑这里就不细述了(以后有时间再补充),大概思路就是通过编码 | 解码将路由path变化到一个token数组的过程,让程序能准确辨认并处理子路由、动态路由、路由参数等情景。
// 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)
}
完成上一步后,程序会对originalRecord做判断,如果有则将匹配器(matcher
)放入alias中;没有则认为第一个record
为originalMatcher
,而其他则是当前路由的aliases
,这里要注意点是当originalMatcher
和matcher
不等时,说明此时matcher是由别名记录产生的,将matcher放到originalMatcher的aliases
中。再往后就是为了避免嵌套调用而删掉不冗余路由。
if (mainNormalizedRecord.children) {
const children = mainNormalizedRecord.children
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
)
}
}
再往下就是遍历当前matcher的children matcher做同样的初始化操作。
// 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)
// }
insertMatcher(matcher)
再看看insertMatcher
定义:
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
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher)
}
源码在添加matcher前还要对其判断,以便重复插入。当满足条件时,将matcher增加到matchers数组中;另外,假如matcher并非别名record时,也要将其记录到matcherMap
中,matcherMap
作用是通过名字快速检索到对应的record对象,在增加、删除、查询路由时都会用到。
至此addRoute
逻辑基本完结了,最后返回original matcher集合,得到文中开头截图的matchers。
location
(路由路径对象,可以是path 或 name与params的组合;currentLocation
(当前路由matcher location,这个在外层调用时已经处理好)方便大家理解,这里还是先举个例子:
export const router = createRouter(options)
const matchers = createRouterMatcher(options.routes, options)
console.log('obj:', matchers)
输出:
这里大家可能会有个疑问,假如2个参数的路由不一致会以哪个为准?
其实这是个伪命题,matcher
内部的resolve
方法和平时我们外部调用的router resolve方法不一样,内部这个resolve的2入参数默认指向同一个路由而不管外部的业务逻辑如何,在外部router resolve已经把第二个参数处理好,所以才有上面截图的效果。
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) {
// match by name
} else if ('path' in location) {
// match by path
} else {
// match by name or path of current route...
}
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),
}
}
上面为省略源码,无非就是通过3种方式(通过name、path、当前路由的name或path)查找matcher,最后返回一个完整的信息对象。
matcherRef
(路由标识,可以是字符串或object)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)
}
}
}
删除路由matcher逻辑也不复杂,先干掉本路由matcher,然后再递归干掉其子路由和别名路由。
function getRoutes() {
return matchers
}
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
上面说过,matcherMap
是一个map结构的内存变量,能通过name快速检索到指定的matcher。
好了,相信小伙伴们都对vue router 4
的matcher
有总体的认识和理解,这节先到这里,下节我们会聊下vue router 4
中核心能力之一:源码中有关Web History API能力的部分,看看它是如何把原生能力完美结合起来的。
最后感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。