什么是route
在上一篇文章中介绍了#createRouter, 知道了在其返回的router中有一部分用于操作route的方法, 为了明白这些方法到底在做什么, 首先需要知道到底什么是route.
先来查看#addRoute的方法实现
// 添加一个新路由
function addRoute(
parentOrRoute: RouteRecordName | RouteRecordRaw,
route?: RouteRecordRaw
){
let parent: Parameters<typeof matcher['addRoute']>[1] | undefined
let record: RouteRecordRaw
if (isRouteName(parentOrRoute)) {
parent = matcher.getRecordMatcher(parentOrRoute)
record = route!
} else {
record = parentOrRoute
}
return matcher.addRoute(record, parent)
}
其中最核心的功能代码为return matcher.addRoute(record, parent), 通过仔细阅读代码, 可以发现#addRoute中的route是一个RouterRecordRaw对象, 其实现添加逻辑的核心在于调用了matcher#addRouer. 我们已经抓住了理清route结构的线头, 接下来先来看下RouterRecordRaw的结构.
export type RouteRecordRaw =
| RouteRecordSingleView
| RouteRecordSingleViewWithChildren
| RouteRecordMultipleViews
| RouteRecordMultipleViewsWithChildren
| RouteRecordRedirect
通过代码, 发现RouterRecordRaw是一个类型集合, 看起来十分复杂, 但只要仔细查看就会发现他们都拓展自同一个类型_RouteRecordBase. 然后在该类型的基础上控制一些字段的组合.
export interface _RouteRecordBase extends PathParserOptions {
path: string
redirect?: RouteRecordRedirectOption
alias?: string | string[]
name?: RouteRecordName
beforeEnter?:
| NavigationGuardWithThis<undefined>
| NavigationGuardWithThis<undefined>[]
meta?: RouteMeta
children?: RouteRecordRaw[]
props?: _RouteRecordProps | Record<string, _RouteRecordProps>
}
export interface RouteRecordSingleView extends _RouteRecordBase {
component: RawRouteComponent
components?: never
children?: never
redirect?: never
props?: _RouteRecordProps
}
通过阅读以上代码, 我们基本可以确定, RouterRecordRaw就是我们定义的一个页面路由. 通常是这样的.
const addressRoute: RouteRecordRaw[] = [
{
path: "/address",
name: "addressList",
component: Layout,
meta: { hidden: false },
redirect: "/address/list",
children: [
{
path: "list",
component: () => import("@/views/address/index.vue"),
meta: { hidden: false, title: "地址管理", keepAlive: false },
},
],
},
];
在知道了route是什么后, 我们需要解决另一个问题, matcher是什么? 为什么实现添加route需要调用他的方法.
什么又是matcher
在阅读了#addRoute的代码后, 发现其调用matcher#addRouter来实现添加路由逻辑. 因此可以大胆假设, router中所有对route的操作都是通过matcher进行的. 接下来我们进行小心求证. 已知router中关于route操作的方法共有4个, 分别是#addRoute, #removeRoute, #getRoute和#hasRoute. 其中#addRoute方法已经确定了, 接下来依次检查其他三个方法的实现.
#removeRoute
// 移除路由
function removeRoute(name: RouteRecordName) {
const recordMatcher = matcher.getRecordMatcher(name)
if (recordMatcher) {
matcher.removeRoute(recordMatcher)
} else if (__DEV__) {
warn(`Cannot remove non-existent route "${String(name)}"`)
}
}
- #getRoutes
// 获得所有路由
function getRoutes() {
return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
}
#hasRoute
// 判断指定路由是否存在
function hasRoute(name: RouteRecordName): boolean {
return !!matcher.getRecordMatcher(name)
}
通过查看代码, 我们并不惊讶的发现, 所有关于route的操作都是对matcher相关方法的二次封装. 那么matcher到底是何方神圣呢?
揭开matcher的面纱
在createRouter的开头, matcher就暴露了自己.
// 创建路由匹配器
const matcher = createRouterMatcher(options.routes, options)
可以看到他是#createRouterMatcher接收options参数对象后的返回值. 那么我们就有必要看一下#createRouterMatcher又是什么了.
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher
从函数签名中可以发现, #createRouterMatcher返回一个RouterMatcher.
export 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
}
RouterMathcer是一个接口, 定义了对route的操作. 继续阅读#createRouterMatcher代码如下
// matchers 存放所有路由记录
const matchers: RouteRecordMatcher[] = []
// matcherMap 建立路由标识和对应路由的匹配
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
在#createRouterMatcher里, 首先创建了一个RouteRecordMatcher数组和一个键为RouteRecordName, 值为RouteRecordMatcher的map. 这两个类型如同其名字一样, 分别是路由记录标识和路由记录匹配器. RouteRecordName是一个string或symbol, RouteRecordMatcher的具体结构暂且不表.
我们可以看到, #createRouterMatcher并没有暴露matchers和matcherMap, 而是形成了一个闭包. 接着我们继续阅读暴露出去的方法.
- 首先阅读两个简单的,
#getRouters和#hasRoute
// 获得路由数组matchers
function getRoutes() {
return matchers
}
// 判断指定路由是否存在
function hasRoute(name: RouteRecordName): boolean {
return !!matcher.getRecordMatcher(name)
}
function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
可以看到#getRouters只是简单的返回了matchers, 而#hasRoute则调用了matcher#getRecordMatcher来判断路由是否存在, matcher#getRecordMatcher则是通过matcherMap#get来进行判断的. 这样我们就可以清楚的明白matchers和matcherMap的作用了, 其中matchers是存放所有路由记录的数组, matcherMap是通过标识来快速索引路由记录的一个map.
- 然后是
#removeRoute
function removeRoute(matcherRef: RouteRecordName | RouteRecordMatcher) {
// 判断matcherRef的类型
if (isRouteName(matcherRef)) {
// 如果是RouteRecordName需要先get获取对应的RouteRecordMatcher
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)
}
}
}
#removeRoute实现的功能就是将和matcherRef匹配的RouteRecordMatcher从matchers和matcherMap中删除, 同时如果其有子路由或别名路由, 也要一并删除.
#addRoute, 相对比较复杂
function addRoute(
record: RouteRecordRaw,
parent?: RouteRecordMatcher,
originalRecord?: RouteRecordMatcher
) {
// 如果传入了originalRecord, 即该路由是别名路由时, isRootAdd为假, 否则为真
const isRootAdd = !originalRecord;
// 标准化路由记录
const mainNormalizedRecord = normalizeRouteRecord(record);
// 开发环境下, 检查路由是否是一个没有name属性的空路径子路由, 如果是, 报错
if (__DEV__) {
checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent);
}
// aliasOf 表示此记录是否是其他路由记录的别名, 如果不是, 其值为 undefined
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) {
const { path } = normalizedRecord;
// 当父路由存在且子路由不以 '/' 开头, 当父路由不以 '/' 结尾时需要在父路由末尾添加一个 '/'
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);
// 当该路由为别名路由时, 需要将其添加进源路由的别名中用于之后的路由移除
if (originalRecord) {
originalRecord.alias.push(matcher);
if (__DEV__) {
checkSameParams(originalRecord, matcher);
}
} else {
// 否则, 第一个record就是源路由, 而其他路由是别名路由(如果存在)
// 仅当originalMatcher为空时进行第一次赋值, 之后originalMatcher均不为空
// 达到了将normalizedRecords数组的第一个路由赋值给originalMatcher的目的
originalMatcher = originalMatcher || matcher;
// 将其他路由添加进源路由的别名路由数组中
if (originalMatcher !== matcher) originalMatcher.alias.push(matcher);
// 移除已存在的命名路由
if (isRootAdd && record.name && !isAliasRecord(matcher))
removeRoute(record.name);
}
// 对子路由递归添加
if (mainNormalizedRecord.children) {
const children = mainNormalizedRecord.children;
for (let i = 0; i < children.length; i++) {
addRoute(
children[i],
matcher,
originalRecord && originalRecord.children[i]
);
}
}
// 将别名路由指向源路由
originalRecord = originalRecord || matcher;
// 将新路由插入到matcers和matcherMap中
insertMatcher(matcher);
}
// 如果没有源routerMatcher, 那么返回一个空函数
// 返回一个可以将源路由 matcher 删除的方法
return originalMatcher
? () => {
removeRoute(originalMatcher!);
}
: noop;
}
虽然addRouter看起来一大坨代码很复杂, 但可以对其核心逻辑进行简化
- 将传入的
record进行格式化, 将格式化后的路由记录添加到normalizedRecords中. - 处理
record的别名情况, 将别名路由也添加到normalizedRecords中. - 遍历
normalizedRecords, 使用#createRouteRecordMatcher为每一个格式化后的路由记录normalizedRecord创建一个路由记录匹配器matcher. - 记录别名路由, 用于之后的删除
- 递归添加
normalizedRecord的子路由 - 调用
#insertMatcher将matcher插入到matchers和matcherMap中.
整个过程中有三个关键的方法, 分别是
normalizeRouteRecordcreateRouteRecordMatcherinsertMatcher
其中#insertMatcher是在#createRouterMatcher中定义的函数, 先阅读其代码
function insertMatcher(matcher: RouteRecordMatcher) {
let i = 0;
while (
i < matchers.length &&
// mather和mathers[i]进行比较
comparePathParserScore(matcher, matchers[i]) >= 0 &&
// mather的path与matcher[i]不同或mather不是matcher[i]的孩子
(matcher.record.path !== matchers[i].record.path ||
!isRecordChildOf(matcher, matchers[i]))
)
i++;
matchers.splice(i, 0, matcher);
// 仅当matcher不是一个别名路由时才添加进matcherMap中
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
其逻辑即为通过比较将传入的matcher插入到mathers和matcherMap中.
剩下的#createRouteRecordMatcher作用为将路由记录的path属性格式化, 用于比较不同路由记录间的优先度. #normalizeRouteRecord的作用为将传入的路由记录格式化, 为其缺失的属性添加默认值. #createRouteRecordMatcher涉及到路径的格式化, 因此留到之后讲解.
export 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 || [],
insveGuards: new Set(),
updtances: {},
leaateGuards: new Set(),
enterCallbacks: {},
components:
'components' in record
? record.components || null
: record.component && { default: record.component },
}
}
在定义好这些操作route的方法后, #createRouterMatcher对传入的routes进行遍历, 调用#addRoute将每个传入的route添加到matchers和matcherMap中. 然后将这些方法暴露出去供router进行调用.
// 添加所有接收的route到 matchers 和 matcherMap 中
routes.forEach(route => addRoute(route))
// 返回定义的操作
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
总结
本篇文章介绍了vue-router中route的存储方式和基本的操作方法实现逻辑.