咳咳,各位靓仔靓女,老司机要开车了,今天咱们聊聊 Vue Router 3 的骨灰级玩法:createRouter 函数的底层实现和 history 管理。准备好了吗?Let’s go!
第一站:createRouter 的身世之谜
首先,我们要搞清楚 createRouter 到底是个什么玩意儿。简单来说,它就是 Vue Router 的入口,负责创建 Router 实例,并配置各种选项。但它内部到底发生了什么呢?
function createRouter(options) {
const { history, routes } = options;
// 1. 创建 matcher,负责路由匹配
const matcher = createMatcher(routes);
// 2. 创建 Router 实例
const router = new Router({
history,
matcher,
});
return router;
}
这段代码是不是看起来很简洁?但魔鬼就藏在细节里。我们逐一分解:
-
options参数:这个对象包含了创建 Router 实例所需的所有配置,最重要的就是history和routes。history:决定了路由的工作模式,比如 Hash 模式、History 模式等。routes:一个数组,定义了应用程序的所有路由规则。
-
createMatcher(routes):这是一个关键函数,负责根据路由配置创建matcher对象。matcher才是真正干活的,它负责将 URL 路径与路由规则进行匹配,找到对应的组件。我们稍后会深入探讨createMatcher。 -
new Router({ history, matcher }):这就是创建 Router 实例的核心代码。Router类会接收history和matcher作为参数,并初始化各种内部状态。
第二站:路由匹配器 createMatcher 的秘密花园
createMatcher 的作用是把我们定义的路由配置,转化成高效的路由查找工具。简单来说,就是把路由规则“编译”成一种方便快速查找的数据结构。
function createMatcher(routes) {
const routeMap = {}; // 用于存储路由信息的对象
// 1. 添加路由记录
function addRouteRecord(route, parent) {
const path = parent ? `${parent.path}/${route.path}` : route.path;
const record = {
path,
component: route.component,
name: route.name,
meta: route.meta,
parent,
children: route.children || [],
};
// 将路由记录添加到 routeMap 中
routeMap[path] = record;
// 递归处理子路由
if (route.children) {
route.children.forEach((child) => addRouteRecord(child, record));
}
}
// 遍历路由配置,添加路由记录
routes.forEach((route) => addRouteRecord(route, null));
// 2. 匹配路由
function match(location) {
return routeMap[location]; // 直接从 routeMap 中查找
}
return {
addRoute: addRouteRecord, // 动态添加路由
match, // 匹配路由
};
}
代码分析:
-
routeMap对象:这是一个核心的数据结构,用于存储所有路由记录。它的 key 是路由路径,value 是对应的路由记录对象。 -
addRouteRecord(route, parent)函数:这个函数负责递归地添加路由记录。- 它会根据路由配置创建一个
record对象,包含路由的各种信息,如路径、组件、名称、元数据等。 - 如果存在父路由,它会将父路由的路径与当前路由的路径拼接起来,形成完整的路径。
- 它会将路由记录添加到
routeMap中。 - 如果存在子路由,它会递归调用
addRouteRecord函数处理子路由。
- 它会根据路由配置创建一个
-
match(location)函数:这个函数负责根据 URL 路径匹配路由记录。- 它直接从
routeMap中查找对应的路由记录。
- 它直接从
-
返回值:
createMatcher函数返回一个对象,包含addRoute和match两个方法。addRoute用于动态添加路由。match用于匹配路由。
注意:这个版本的 createMatcher 非常简化,实际的 Vue Router 实现要复杂得多,需要处理更复杂的路由匹配逻辑,比如:
- 动态路由:
/users/:id - 通配符路由:
/users/* - 路由优先级:当多个路由都匹配时,需要确定哪个路由应该被选中。
路由匹配算法的优化
为了提高路由匹配的效率,Vue Router 采用了多种优化技术,比如:
- 前缀树 (Prefix Tree):用于存储路由路径,可以快速查找具有相同前缀的路由。
- 缓存 (Cache):缓存最近访问的路由,避免重复匹配。
第三站:history 管理的幕后英雄
history 对象负责管理浏览器的历史记录。Vue Router 提供了多种 history 实现,包括:
- HashHistory:使用 URL 的 hash 部分 (
#) 来模拟路由。 - HTML5History:使用 HTML5 History API (
pushState和replaceState) 来实现真正的路由。 - AbstractHistory:用于非浏览器环境,比如 Node.js。
我们以 HTML5History 为例,看看它是如何工作的。
class HTML5History {
constructor() {
this.current = {
path: window.location.pathname,
};
// 监听 popstate 事件
window.addEventListener('popstate', () => {
this.transitionTo(window.location.pathname);
});
}
push(path) {
history.pushState({ path }, null, path);
this.transitionTo(path);
}
replace(path) {
history.replaceState({ path }, null, path);
this.transitionTo(path);
}
transitionTo(path) {
// 1. 匹配路由
const matched = this.router.matcher.match(path);
// 2. 更新 current 路由
this.current = {
path,
matched,
};
// 3. 调用路由守卫
// ...
// 4. 更新视图
// ...
}
}
代码分析:
-
构造函数:
- 初始化
current路由对象,记录当前路径。 - 监听
popstate事件,当用户点击浏览器的前进/后退按钮时,会触发该事件。
- 初始化
-
push(path)和replace(path)方法:- 调用
history.pushState或history.replaceState方法更新浏览器的历史记录。 - 调用
transitionTo(path)方法进行路由切换。
- 调用
-
transitionTo(path)方法:- 使用
matcher.match(path)匹配路由。 - 更新
current路由对象。 - 调用路由守卫 (beforeEach, beforeRouteEnter, afterEach)。
- 更新视图,渲染对应的组件。
- 使用
History 模式的优缺点
| 特性 | Hash 模式 | HTML5 History 模式 |
|---|---|---|
| URL 格式 | /#/path/to/resource |
/path/to/resource |
| SEO | 不利于 SEO | 有利于 SEO |
| 服务器端配置 | 不需要服务器端配置 | 需要服务器端配置,确保所有路由都指向同一个入口文件 |
| 兼容性 | 兼容性好 | 需要浏览器支持 HTML5 History API |
| 用户体验 | URL 中包含 #,用户体验较差 |
URL 简洁美观,用户体验好 |
第四站:Router 实例的职责
Router 实例是 Vue Router 的核心,负责管理路由状态、处理导航、执行路由守卫等。
class Router {
constructor(options) {
this.history = options.history;
this.matcher = options.matcher;
// 将 Router 实例绑定到 History 对象
this.history.router = this;
this.beforeEachHooks = [];
this.afterEachHooks = [];
// 初始化路由
this.history.transitionTo(this.history.current.path);
}
push(path) {
this.history.push(path);
}
replace(path) {
this.history.replace(path);
}
beforeEach(fn) {
this.beforeEachHooks.push(fn);
}
afterEach(fn) {
this.afterEachHooks.push(fn);
}
}
代码分析:
-
构造函数:
- 接收
history和matcher作为参数。 - 将
Router实例绑定到History对象,方便History对象调用Router的方法。 - 初始化路由,调用
history.transitionTo方法进行首次路由切换。 - 初始化路由守卫钩子函数数组。
- 接收
-
push(path)和replace(path)方法:- 调用
history.push和history.replace方法进行导航。
- 调用
-
beforeEach(fn)和afterEach(fn)方法:- 用于注册全局路由守卫。
第五站:路由守卫的战场
路由守卫是 Vue Router 提供的一种机制,允许我们在路由切换前后执行一些操作,比如:
- 权限验证:判断用户是否有权限访问某个路由。
- 数据预取:在进入某个路由之前,先加载需要的数据。
- 记录日志:记录用户的访问行为。
Vue Router 提供了三种路由守卫:
- 全局守卫:
beforeEach,afterEach - 路由独享守卫:
beforeEnter - 组件内守卫:
beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave
全局守卫
beforeEach:在每次路由切换之前执行。afterEach:在每次路由切换之后执行。
路由独享守卫
beforeEnter:只在进入路由时执行。
组件内守卫
beforeRouteEnter:在组件被创建之前执行,不能访问this。beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用。beforeRouteLeave:在离开路由时执行。
路由守卫的执行顺序:
- 离开守卫:
beforeRouteLeave - 全局前置守卫:
beforeEach - 路由独享守卫:
beforeEnter - 组件内守卫:
beforeRouteEnter - 全局解析守卫:
beforeResolve - 导航被确认
- 全局后置钩子:
afterEach - DOM 更新
- 通过传给
next的回调函数来调用beforeRouteEnter守卫中访问组件实例。
第六站:动态路由的玩法
动态路由允许我们定义带有参数的路由,比如 /users/:id。
const routes = [
{
path: '/users/:id',
component: User,
},
];
在组件中,我们可以通过 $route.params 对象访问路由参数。
<template>
<div>
<h1>User ID: {{ $route.params.id }}</h1>
</div>
</template>
第七站:总结与升华
我们今天深入剖析了 Vue Router 3 的 createRouter 函数的底层实现和 history 管理。
createRouter函数负责创建 Router 实例,并配置各种选项。createMatcher函数负责将路由配置转化成高效的路由查找工具。history对象负责管理浏览器的历史记录。Router实例负责管理路由状态、处理导航、执行路由守卫等。- 路由守卫允许我们在路由切换前后执行一些操作。
- 动态路由允许我们定义带有参数的路由。
希望今天的分享能帮助大家更深入地理解 Vue Router 的工作原理。记住,源码之下,了无秘密!
今天的旅程就到这里,各位,下课!下次有缘再见!