背景介绍
在之前的一篇文章中vue-router 如何做到页面切换?, 源码解析曾经说过,页面的切换其实就是一个找组件的过程,也就是 vue-router 里面的 Matcher, 看过 vue-router4 的源码后,发现 matcher 其实是非常重要的一环,可以说是和 history(历史模式) 构成了 vue-router 的两个核心,回过头来看一下 vue-router4 中的创建 router 操作
const router = VueRouter.createRouter({
history: createWebHistory(),
routes,
});
其实也暗示着 history(对应历史模式) 和 routes(对应 matcher) 是很重要的,话不多说,本篇文章将会介绍 vue-router4 里面的 matcher
注:本篇文章关于 matcher 的解析和源码均对应 vue-router4 即 vue3 版本的 router
注意区分文章中的, routes 和 route, route 指 { path: "/", Redirect: "/test" }, routes 指 [route,route,...]
addRoute
当我们创建 router 实例时,会从 createRouter 将 options.routers 携带到 createRouterMatcher 并在其中初始化 matcher,代码如下
// code
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// source code
function createRouter(options: RouterOptions): Router {
// options.routes 即 routes = [{ path: "/",... },{ path: "/about",...}]
const matcher = createRouterMatcher(options.routes, options);
// ...
}
重点在 createRouterMatcher 里面的 addRoute,注意是一条条 route 处理的,整个 matcher 中的封装操作将在里面完成,而 route 的解析将告一段落,至于封装的意思就是,刚开始传进去的 routes 将不会是 vue-router4 中真正意义
上使用的 routes,matcher 部分会将其进一步处理,像别名这些都要进一步处理
function createRouterMatcher(
routes: RouteRecordRaw[],
globalOptions: PathParserOptions
): RouterMatcher {
// ...
// 添加初始化后的 routes, 或者说封装后的 routes
routes.forEach((route) => addRoute(route));
// ...
}
addRoute 其实在 createRouterMatcher 里面做了下面 5 件事
- 初步修缮 routes - normalize
- 合并 option
- 处理别名 - alia
- 第二次修缮 routes
- 返回 真 -
routes
接下来就针对以上几点去介绍
初步修缮 routes - normalize
这个地方主要是负责路由组件传参
这块的内容,具体功能可以看 vue-router4 的文档,而文章这里将会介绍它是怎么做的,为什么要这么做
matcher 在 normalize 这里入口是一个工厂函数
(小细节了啊),但是我们要知道 vue-router4 是用 typescript 写的,它的返回值也是有定义的,让我们看看这个函数
function normalizeRouteRecord(record: RouteRecordRaw): RouteRecordNormalized {
return {
path: record.path,
redirect: record.redirect,
// ...
};
}
它返回值是一个叫 RouteRecordNormalized
的接口 interface, 它的这个工厂函数
里面就是返回一个键与 RouteRecordNormalized
相对应的一个对象,不知道这么做是不是有让读者更方便去看给 RouteRecordNormalized
中的哪个属性如何赋值的意思,像在 java 里面很有可能就是 RouteRecordNormalized(path, redirect, ...)
构造函数创建了,当然前提是 RouteRecordNormalized
得是个 Class(不知道用 ts 的人用 Class 的多不多?)
那么这个 RouteRecordNormalized
的目的就是返回处理后的 copy 值(又是 JS 老生常谈的对象引用值问题),这个时候我们原先设定好的 route 在这里就进行了第一步的处理
// code
const routes = [{ path: "/", component: Home }];
// source code
// route: RouteRecordRaw to route: RouteRecordNormalized
那么这个工厂函数
它对我们一开始传进来的 route 做了什么处理呢?接下来就是处理过程
处理过程
上面这两个类型在 vue-router4 的 API 参考文档里面有,处理过程就是对 RouteRecordRaw
to RouteRecordNormalized
赋初值以及新增属性,下面是处理前后的 route
比较
// code
const route = { path: "/", component: Home };
// 对应下面的
const route = {
path: "/",
redirect: undefined,
name: undefined,
meta: undefined,
alias: undefined,
beforeEnter: undefined,
props: undefined,
children: undefined,
};
// source code: normalizeRouteRecord 处理后
const route = {
path: "/",
redirect: undefined,
name: undefined,
meta: {},
beforeEnter: undefined,
props: undefined,
children: [],
// 新增部分
aliasOf: undefined,
instances: {},
leaveGuards: new Set(),
updateGuards: new Set(),
enterCallbacks: {},
components: undefined,
};
这个处理过程比较重要的一个点就是 props
的处理,这里可以看一下 vue-router-API-props 参考, 文档里面的要求可以在源码这里体现出来,简简单单来欣赏一下这段源码
function normalizeRecordProps(
record: RouteRecordRaw
): Record<string, _RouteRecordProps> {
const propsObject = {} as Record<string, _RouteRecordProps>;
// 重定向的路由不会有 props, 因此其 props 值应为 false
const props =
(record as Exclude<RouteRecordRaw, RouteRecordRedirect>).props || false;
if ("component" in record) {
propsObject.default = props;
} else {
// 小细节: props 函数模式能应用到对应的命名视图组件里面
for (const name in record.components)
propsObject[name] = typeof props === "boolean" ? props : props[name];
}
return propsObject;
}
上面这段源码其实就是针对了 props
的三种模式
- 布尔模式
- 对象模式
- 函数模式
在 props 的获取中,对于重定向路由
做了特殊处理,默认设置为 false, 但是如果手贱设置了 props 属性,不知道会不会对 vue-router 造成很大的影响?毕竟 vue-router4 其实对 route 的值的约束其实很小
route 指 { path: "/", Redirect: "/test" }
// code
{ path: "/", Redirect: "/test" }
// source code
{ path: "/", Redirect: "/test", props: false }
对象模式和布尔模式是这样处理的
if ('component' in record) {
propsObject.default = props
这里我们要注意一个小细节, 是设定的 propsObject.default = props
, 为什么是 default ? 因为要兼容命名视图
,所以无论你一开始的 route 是否是命名视图
模式,处理过后的 props 一定有 propsObject.default = xxx
命名视图就不用说了,看一下处理后会是怎么样
for (const name in record.components)
propsObject[name] = typeof props === "boolean" ? props : props[name];
// code
const props = { default: true, other: true }
// source code
const propsObject = { default: true, other: true }
vue-router 文档中没有的小细节
从上面源码中其实我们可以看到,当 route 是命名视图模式时,如果 props 是 boolean 实际上会被应用到每个组件上,比如下面的例子
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: true
}
]
// 等于下面这个
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: true }
}
]
总结
vue-router 的 matcher
- createRouterMatcher - 创建 matcher 并初始化
- addRoute - 初始化传入的 route
- normalizeRouteRecord - 封装传入的 route, 相当于
升级
,对应 API 文档的 RouteRecordNormalized - normalizeRecordProps - 处理 props,对应文档的路由传参部分
结尾也回归主题一下吧,想要找到组件那你找不就完了?那如果你的目的是这个,那看到 addRoute
那里你就可以结束 matcher 的部分了,那 addRoute
了,内存就有一个路由映射表了,剩下的就找不就完事了?但文章其实要讲的更加细节(没错,是细节怪)找到正确的路由才算成功,那路由的处理也算是一部分
vue-router matcher 部分的 normalize 就先讲到这里,讲了 matcher 对 route 的第一步处理,如果你对 vue-router4 的源码感兴趣的话,觉得文章讲的还行,可以看之后发的几篇源码解析(关注就能看到后续更新)
- vue-router 如何找到待渲染的 vue 组件?vue-router Matcher 解析(二)- 没写 - 处理别名
- vue-router 如何找到待渲染的 vue 组件?vue-router Matcher 解析(三)- 没写 - 第二次处理 routes