咳咳,各位靓仔靓女,老司机要开车了,今天咱们聊聊 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 的工作原理。记住,源码之下,了无秘密!
今天的旅程就到这里,各位,下课!下次有缘再见!