前情提要
之前的文章vue-router 如何找到待渲染的 vue 组件?vue-router Matcher 解析(一)讲了, vue-router 找到待渲染的 vue 组件时,对 route 的处理过程,因此本篇文章将会默认 route 已经被处理了,即为 RouteRecordNormalized
路由记录的标准版本
本篇文章会介绍 matcher 处理 route 的别名部分, 即 vue-router 文档的重定向和别名
注:本篇文章关于 matcher 的解析和源码均对应 vue-router4 即 vue3 版本的 router
注意区分文章中的, routes 和 route, route 指 { path: "/", Redirect: "/test" }, routes 指 [route,route,...]
别名处理
参考文档中对别名处理 API 的介绍, vue-router 文档 - 别名
总之别名最后实现的效果就是下面这样的
const route = {
path: "/users",
component: User,
// 英语水平太差, 开摆了, 随便找一个单词
alias: "/humans",
children: {
{ path: '', component: UserList, alias: ['/people', 'list'] },
}
}
上面的 route 能匹配下面的路径
// - /users
// - /people
// - /users/list
// - /humans/list
// - /humans
所以别名(alias)在 vue-router 上是一种怎样的存在方式呢?怎么操作的呢?前排提示, 其实再复制语法路由记录, 而从上面其实可以看出来, vue-router 其实在 alias(别名的处理上) 做了两件事
- 添加别名路由
- 递归更新别名子路由
接下来将细嗦一下 vue-router 是怎么解决上面这两个问题的
添加别名路由
在 router/src/matcher/index.ts 86 处会对 normalizedRecords(routes 的标准化版本数组), 进行一个数组化
处理, 其实就是为了方便我们添加别名,所以默认加了别名,可以参考一下面的一个数据结构描述
// 源码
const routes = { path: "/users", alias: "/humans" };
// vue-router 处理后
const normalizedRecords = [{ path: "/users" }, { path: "/humans" }];
vue-router 对添加别名路由的操作其实也是很简单粗暴,一个 for 循环,像下面这样
if ("alias" in record) {
const aliases =
typeof record.alias === "string" ? [record.alias] : record.alias!;
for (const alias of aliases) {
normalizedRecords.push({
// alias route: RouteRecordNormalized
});
}
}
注意一个小细节,看下面源码
const aliases =
typeof record.alias === "string" ? [record.alias] : record.alias!;
但这里应该是负责 record.alias 为 string[] 的情况的, alias 在 route 对应的接口 _RouteRecordBase
中实际上是一个可选的属性,即可能会是 undefined, 因此这里其实是对其做的一个空值处理, 表示 record.alias 一定会有值,不可能会是 null 或者 undefined, 但实际上即使 record.alias = undefined || null, vue-router 也是能运行的但是会报错, 过不了 for (const alias of aliases) {//...}
添加子路由并不是把 path 改了就行了,还得给子路由找爸爸
, components 和 aliasOf 就是需要依靠 originalRecord 去判断, 如果存在父路由, 子路由就需要保留这个值, 这个其实和递归更新子路由有关
normalizedRecords.push(
assign({}, mainNormalizedRecord, {
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
aliasOf: originalRecord ? originalRecord.record : mainNormalizedRecord,
}) as typeof mainNormalizedRecord
);
这里可以讲一下 vue-router 有一个设计的巧妙之处
for (const normalizedRecord of normalizedRecords) {
matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
if (originalRecord) {
originalRecord.alias.push(matcher);
} else {
originalMatcher = originalMatcher || matcher;
}
originalRecord = originalRecord || matcher;
}
以 const routes = { path: "/users", alias: "/humans" };
为例,对应上面源码的一个数据结构, originalRecord 指的就是 "/users"
const normalizedRecords = ["/users", "/humans"];
它的目的就是使 "/users" 生成的 Matcher 需要和 "/humans"(alias) 生成的 Matcher 建立联系, 也就是 originalRecord.alias.push(matcher);
这一步, "/user" 作为 normalizedRecord 第一次遍历时是没有 originalRecord, 然后通过 ||
赋值, ||
其实可以叫做选择运算符, 它会选择第一个为真的值, 比如
console.log(false || 1); // 1
console.log(1 || false); // 1
所以第一次遍历的时候, 遍历 original
"/users", originalRecord = undefined || matcherByUser, 而第二次遍历的时候就是 originalRecord = matcherByUser || matcherByHuman, 因此可以整个的遍历的 original
都将指向 "/users"
递归更新别名子路由
递归更新别名子路由,里用到了多叉树遍历算法(leetcode 有详细的解法, labuladong 也讲过),如果把源码抽调出来的话你可以看到, vue-router4 的递归其实和多叉树遍历框架同根同源
function addRoute() {
for (const normalizedRecord of normalizedRecords) {
if ("children" in mainNormalizedRecord) {
const children = mainNormalizedRecord.children;
for (let i = 0; i < children.length; i++) {
addRoute();
}
}
}
}
// 多叉树遍历框架
const traverse = (root) => {
if (root === null) {
return;
}
for (let i = 0; i < root.children.length; i++) {
traverse(root.children[i]);
}
};
之所以多了一层循环是因为, vue-router 的 alias 和 original 同时分裂
的, 前面也说过像 route 一开始就会做数组化处理,至此达成闭环
// 源码
const routes = { path: "/users", alias: "/humans" };
// vue-router 处理后
const normalizedRecords = [{ path: "/users" }, { path: "/humans" }];
通过这部分我们可以知道, vue-router 处理 routes 这种多叉树结构时用的是递归式的遍历框架
总结
最后讲一下阅读 vue-router 时的一些小技巧吧,一开始看 matcher 这部分的代码的时候,看了各个部分的一个调用的关系,看着看着就掉入了细节
陷阱,浪费了很长时间,长时间纠结于设计者
在部分源码的考虑,和 TS 的类型,想要快速理解一部分源码的使用以及设计者的考虑,可以尝试去 Chrome 去调试,在知道了源码模块之间的调用关系之后就可以,设计测试用例,比如 vue-router 文档中给出的 route, 就是非常好的测试用例, 然后运行到 Chrome 去调试, Chrome 的调试优于 Vs code 调试, 加上 vite 的加持, 可以说快到起飞, 在打断点一行行运行的时候, 你可以看到设计者在源码上的思考,调试这种动态的过程,是比你直接看源码要高效很多的
vue-router matcher 部分的 alias(别名处理) 就先讲到这里,如果你对 vue-router4 的源码感兴趣的话,觉得文章讲的还行,可以看之后发的几篇源码解析(关注就能看到后续更新), Matcher 部分文章如下
- vue-router 如何找到待渲染的 vue 组件?vue-router Matcher 解析(一))- 标准化 route
- vue-router 如何找到待渲染的 vue 组件?vue-router Matcher 解析(三)- 没写 - 第二次处理 routes