本文为原创文章,引用请注明出处,欢迎大家收藏和分享💐💐
源码专栏
感谢大家继续阅读《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
。
addRoute
- 定义:初始化matcher
- 接收参数(3个):
record
(需要处理的路由)、parent
(父matcher
)、originalRecord
(原始matcher
),其中后两个是可选项,意思就是只传record则会认为是一个简单路由「无父无别名」并对其处理,假如带上第2、3参数,则还要结合父路由或者别名路由处理 - 返回:单个matcher对象
扩展阅读:别名路由
addRoute关键步骤源码
addRoute的处理过程
流程拆分
标准化处理record和options合并
// 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进行完成初始化的流程中,只要遍历这个数组就行了。
处理alias
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数组的过程,让程序能准确辨认并处理子路由、动态路由、路由参数等情景。
处理originalRecord
// 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做同样的初始化操作。
插入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。
resolve
- 定义:获取路由的标准化版本
- 入参2个:
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,最后返回一个完整的信息对象。
removeRoute
- 定义:删除某个路由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,然后再递归干掉其子路由和别名路由。
getRoutes
- 定义:获取所有的matchers
- 入参:无
- 返回:matchers
源码
function getRoutes() { return matchers } 复制代码
getRecordMatcher
- 定义:获取某个matcher
- 返回:matcher
源码
function getRecordMatcher(name: RouteRecordName) { return matcherMap.get(name) } 复制代码
上面说过,matcherMap
是一个map结构的内存变量,能通过name快速检索到指定的matcher。
落幕
好了,相信小伙伴们都对vue router 4
的matcher
有总体的认识和理解,这节先到这里,下节我们会聊下vue router 4
中核心能力之一:源码中有关Web History API能力的部分,看看它是如何把原生能力完美结合起来的。
最后感谢大家阅览并欢迎纠错,欢迎大家关注本人公众号「似马非马」,一起玩耍起来!🌹🌹