# vue-router-learn
**Repository Path**: wang_xi_long/vue-router-learn
## Basic Information
- **Project Name**: vue-router-learn
- **Description**: 记录学习vue-router相关知识
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2022-04-11
- **Last Updated**: 2022-05-18
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## 简介
vue-router 是 Vue 官方提供的路由管理器, 和 Vue 的核心深度集成, 使得构建单页面变得容易。
由于 Ajax 技术的普及, 页面无刷新有更好的用户体验, 逐渐从单页面应用中的路由, 开始有后端走向前端。
## 路由流程
1. 在 Vue 单页面体系中, ve-router 会有一个监听器, 用来监听浏览器 History 的变化。
2. 通常情况下, 当浏览器地址栏的地址改变或点击游览器前进后退按钮, History 中的历史栈会相应地进行改变。
3. 当监听到 History 变化后, vue-router 就会依据路由表中声明的路由匹配主键, 使用 `routerView`组件进行渲染
## 相关问题
我们该如何监听游览器中的历史记录变化?
```js
// 其一通过监听 window 上 popstate 事件来实现 HTML5Mode
window.addEventListener("popstate", () => {
console.log(window.location.pathname);
});
// 当url # 后的内容发生改变, 页面不刷新,会触发 onhashchange 函数来实现 HashMode
window.onhashchange = function() {
console.log(location.hash);
};
```
## 回顾 vue-router 用法
### 基本用法
1. 用于注册路由表, 并导出
```js
import Vue from "vue"; //引入Vue
import VueRouter from "vue-router"; // 查找vue-router,往上一级一级查找 你安装路由之后不引入相当于做无用功
Vue.use(VueRouter); //vue插件,将VueRouter注册到vue里
import index from "@/views/index/index.vue"; //index首页
export default new VueRouter({
//把我们的路由配置文件暴露出去
mode: "history", //history 默认是hash 标题带# 瞄点
routes: [
//路线 这个存放的是我们路由的配置文件
{
path: "/index", //访问游览器的 路径
name: "index", // 这个是我们给路由起的名称
component: index, //对应的组件
},
],
});
```
2. main.js 导入
```js
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
```
### 动态路由
1. 注册路由
```js
export default new VueRouter({
//把我们的路由配置文件暴露出去
mode: "history", //history 默认是hash 标题带# 瞄点
routes: [
//路线 这个存放的是我们路由的配置文件
{
path: "/index/:id", //访问游览器的 路径
name: "index", // 这个是我们给路由起的名称
component: index, //对应的组件
},
],
});
```
2. 使用动态路由
```html
跳转到index 1 页
跳转到index 2 页
```
使用场景: 商品详情页面的时候,页面结构都一样,只是商品 id 的不同,所以这个时候就可以用动态路由动态。
动态路由就是在 path 路径后加 /:id
动态路由意味着组件实例会被复用, 生命周期钩子不会被重复调用
### 嵌套路由
1. 注册路由
```js
import Profile from "@/views/Profile/index.vue"; //Profile首页
export default new VueRouter({
//把我们的路由配置文件暴露出去
mode: "history", //history 默认是hash 标题带# 瞄点
routes: [
//路线 这个存放的是我们路由的配置文件
{
path: "/index", //访问游览器的 路径
name: "index", // 这个是我们给路由起的名称
component: index, //对应的组件
children: [
{
// 当 /index/profile 匹配成功,
// UserProfile 会被渲染在 User 的 中
path: "profile",
component: Profile,
},
],
},
],
});
```
2. 实际应用界面,通常是多层嵌套的组件组合而成, 使用`children` 来声明嵌套的路由组件
其一是通过监听 window 上 popstate 事件来实现 HTML5Mode
另一种是当 url # 后的内容发生改变, 页面不刷新,会触发 onhashchange 函数来实现 HashMode
### 路由守卫
导航守卫有三种, 全局路由守卫, 路由独享守卫, 组件守卫
| 全局守卫 | 路由独享守 | 组件内守卫 |
| ------------- | ----------- | ----------------- |
| beforeEach | beforeEnter | beforeRouteEnter |
| beforeResolve | | beforeRouteUpdate |
| afterEach | | beforeRouteLeave |
## 实现思路
实现简易版 vue-router 需要至少分两条主线:
1. 路由关系关联表 -- 监听器(两种模式) -- 渲染页面(render 相关组件)
2. 导航守卫 -- 全局守卫 -- 路由独享守卫 -- 组件守卫
## 主线一
此条主线主要是从路由注册, 跳转方面进行讲解, 大致讲解了 Vue 单页面体系中路由管理器的基本实现
### 构建 Router 类, 创建关联路由表
1. 构建 Router 类,
- 构造函数:
- 拿到关联好的路由表,
- 拿到 Html5Mode 实例, 并且将依赖注入,
- init 方法:
- 监听路由的改变,并重新赋值
- 执行页面的第一次跳转
- push 方法:
- 跳转页面 `history.push`
2. 构建 RouterTable 类, 构建路由表 -- 构建路由的关联关系
- 构造函数:
- 创建 Map 用户缓存关联关系
- 初始化 routes
- init 方法:
- 遍历 routes, 将当前 route 添加(addRoute)到 pathMap 中
- 嵌套路由递归处理
- match 方法:
- 匹配当前 path 是否在\_pathMap 当中
### 构建监听器
游览器模式发为 HashMode 和 Html5Mode 两种, 同时监听器也会分为两种
两种 model 建立 history 有相似和差异的地方,采用模板模式,构建一个基类
base 基类, 包括两种 Mode 都有的逻辑, hash 代表 hash mode, html5 代表 html5 mode
#### base 基类
1. 构造函数中, 拿到派生类传递的 router 实例. 依赖反转, 将 routerTable 注入进来
2. listen 方法:
- Vue.use(Router) 调用了 Router.install 方法
- Router.install 方法中将 vue 页面单应用注入(调用 init)
- 执行 Router 的 init 方法, 调用了 History.listen 方法, 传入回调函数 cb
- HistoryBase.listen 方法 接收到回调函数 cb, 进行保存
- 在路由发生改变时, 将当前的路由信息传递给回调函数 cb. 执行回调函数, 将路由注入到 Vue 单页面应用中.
3. transitionTo 方法:
- 接收参数为跳转对应的路由, 在路由表中 match 找到对应的路由
- 执行更新路由
#### Html5Mode
1. 构造函数: 接收构造时传入的参数, 并且传给基类。执行处理事件监听
2. 事件监听处理方法(initListener): 通过监听`popstate`, 跳转到对应的路由
3. 获取路由方法(getCurrentLocation): 返回当前完整的路由 path
4. 路由跳转方法(push): 手动跳转路由, 并且需要手动向 popstate 添加一条记录
### RouterView
1. 获取当前路由, 没有路由就返回 404 页面
2. 将当前路由进行解构,拿到 component 组件返回出去
3. 将 RouterView 注册为全局组件
### RouterLink
1. 编写 RouterLink 组件, 添加点击事件
2. 接收 props to 参数.
3. 在点击事件中跳转到 to 路由
4. 将 RouterLink 注册为全局组件
## 主线二
首先需要理清在一些特定场景下, 会触发哪些导航守卫.
1. bar 跳转到 /foo
- beforeRouteLeave bar 组件离开的守卫
- beforeEach 全局的前置守卫
- beforeRouteUpdate /根组件的改变守卫
- beforeEnter 路由独享守卫
- beforeRouteEnter foo 组件进入的守卫
- beforeResolve 全局的响应守卫
- afterEach 全局的后置守卫
2) 页面初始化触发
- beforeEach 全局的前置守卫
- beforeEnter 独享路由守卫
- beforeRouteEnter 组件的前置守卫
- beforeResolve 全局的响应守卫
- afterEach 全局的后置守卫
#### 使用
```js
// router.js
import Vue from "vue";
import Router from "vue-router";
import Foo from "./pages/Foo";
import Bar from "./pages/Bar";
Vue.use(Router);
const router = new Router({
routes: [
{
path: "/foo",
component: Foo,
beforeEnter(to, from, next) {
// 路由独享守卫 在 router.beforeResolve 之前
console.log("/foo::beforeEnter");
next();
},
},
{ path: "/bar", component: Bar },
],
});
// 解析前
router.beforeEach((to, from, next) => {
console.log("router.beforeEach");
next();
});
// 解析完
router.beforeResolve((to, from, next) => {
console.log("router.beforeResolve");
next();
});
// 解析后
router.afterEach((to, from) => {
console.log("router.afterEach", to, from);
});
export default router;
```
```js
// foo.vue
```
**分析:**
全局路由守卫皆接收一个函数, 同时每个守卫可以出现多次, 此时需要一个队列进行收集.
#### 收集全局路由守卫
```js
// 构造路由的类
export default class Router {
constructor(routes) {
this.beforeHooks = []; // 路由before hooks
this.resolveHooks = []; // 路由resolve hooks
this.afterHooks = []; // 路由after hooks
}
// 对全局路由钩子的收集
beforeEach(fn) {
return registerHook(this.beforeHooks, fn);
}
// 对全局路由钩子的收集
beforeResolve(fn) {
return registerHook(this.resolveHooks, fn);
}
// 对全局路由钩子的收集
afterEach(fn) {
return registerHook(this.afterHooks, fn);
}
}
// 收集路由 和 销毁路由 hooks
function registerHook(list, fn) {
list.push(fn);
return () => {
const i = list.indexOf(fn);
if (i > -1) list.splice(i, 1);
};
}
```
全局路由守卫队列, 添加了三个 `beforeHooks`,`resolveHooks`,`afterHooks` 路由队列
在触发全局路由守卫时, 将对应的路由守卫添加到对应的队列, 并且返回可以销毁该路由守卫的函数
#### 监听 History
前面讲到了 `History` 存在两种模式, `Hash Mode` 和 `Html5 Mode`
1. Hash Mode
```js
import BaseHistory from "./base";
export default class HashHistory extends BaseHistory {
constructor(options) {
super(options);
this.initListener();
}
initListener() {
window.addEventListener(
"hashchange",
() => {
this.transitionTo(this.getCurrentLocation());
},
false
);
}
// 跳转对应的路由
getCurrentLocation() {
let href = window.location.hash;
const searchIndex = href.indexOf("?");
if (searchIndex < 0) {
const hashIndex = href.indexOf("#");
if (hashIndex > -1) {
href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex);
} else href = decodeURI(href);
} else {
href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex);
}
return href;
}
push(hash) {
window.location.hash = hash;
}
}
```
借助 `hashchange` 监听 History 的变化, 并实现了获取当前路由函数.
实现了 push 函数, 用于页面跳转
2. Html5 Mode
```js
import BaseHistory from "./base";
// 代表 html5 model 继承于 BaseHistory
export default class Html5History extends BaseHistory {
constructor(options) {
super(options);
this.initListener(); // 处理事件监听
}
initListener() {
window.addEventListener("popstate", () => {
this.transitionTo(this.getCurrentLocation()); // 跳转对应的路由
});
}
// 两种model 获取路由不同
getCurrentLocation() {
let path = decodeURI(window.location.pathname) || "/";
return path + window.location.search + window.location.hash;
}
push(target) {
this.transitionTo(target); // 需要手动将逻辑 transitionTo 指定的路由
window.history.pushState({ key: +new Date() }, "", target); // 向popstate 添加一条记录
}
}
```
借助 `popstate` 监听 History 的变化, 并实现了获取当前路由函数.
`popstate` 不会自动添加记录, 需要手动添加记录
另外需要手动将逻辑 `transitionTo` 指定的路由
#### 实现首次跳转
1. `Vue.use(Router)` 将 router 注册, 同时触发 `Router.install`,将自己的 router 混入到 vue 中
```js
// router.js
Vue.use(Router) // 将Router注册
// router/router.js
Router.install = function() {
Vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this); // 将vue 页面单应用注入
// 借助Vue的响应式函数将自己的 router 转为 响应式的
Vue.util.defineReactive(this, "_route", this._router.history.current);
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
});
};
* `Router.install`
- 将vue实例 挂载到了this._routerRoot上
- 触发 `router.init`函数,并把 vue 注入
- 将当前路由信息转为响应式的
```
2. `router.init` 函数用于监听路由的改变,并重新赋值, 执行首次跳转
```js
export default class Router {
init(app) {
const { history } = this; // 将history 解构出来
history.listen((route) => {
// 监听路由的改变,并重新赋值,会触发Vue.util.defineReactive 方法
app._route = route;
});
// 执行页面的第一次跳转
history.transitionTo(history.getCurrentLocation());
}
}
```
此函数在前面介绍过, 就不再累述.
3. `HistoryBase.listen` 监听函数, 用于保存 listen 回调函数
```js
export default class HistoryBase {
constructor(router) {
this.router = router;
this.routerTable = router.routerTable; // 依赖反转,将routerTable注入进来
}
listen(cb) {
this.cb = cb;
}
// 跳转对应的路由
transitionTo(target) {
// 判断 target 是否在路由表中
const route = this.routerTable.match(target);
// 当 路由守卫前置执行完成后, 执行更新路由,在render, 再触发全局 afterEach
this.confirmTransition(route, () => {
this.updateRoute(route);
});
}
}
```
4. `history.transitionTo` 执行跳转函数, 接收参数为对应的路由
- 通过`routerTable.match` 函数可以拿到在路由表对应的路由信息
- 执行路由跳转的处理, 如页面初始化的路由守卫
- 当路由守卫执行完成后, 执行更新路由 updateRoute, 再触发全局 afterEach, 最后 render
5. `HistoryBase.confirmTransition` 执行页面初始化路由守卫
- 跳转页面是当前页面直接返回
- 将页面初始化用到的前置守卫组成一个队列
- 执行队列中所有的路由守卫
```js
export default class HistoryBase {
confirmTransition(route, onComplete, onAbort) {
if (route === this.current) {
return;
}
// 页面初始化路由任务队列 -- 执行路由守卫
const queue = [
...this.router.beforeHooks, // 先执行全局 beforeEach
route.beforeEnter, // 路由独享守卫 route.beforeEnter
route.component.beforeRouteEnter.bind(route.instance), // 组件路由 beforeRouteEnter 注意this的指向
...this.router.resolveHooks, // 执行全局 beforeResolve
];
const iterator = (hook, next) => {
// hook (to, from, next) 每一项路由守卫
hook(route, this.current, (to) => {
if (to === false) {
// 判断中断信息是否存在
onAbort && onAbort(to);
} else {
next(to);
}
});
};
runQueue(queue, iterator, () => onComplete());
}
}
// 执行路由守卫队列
export function runQueue(queue, iter, end) {
const step = (index) => {
if (index >= queue.length) {
// 是否执行完毕
end();
} else {
if (queue[index]) {
// 返回迭代器当前路由和next函数(执行下一步)
iter(queue[index], () => {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}
```
queue 是页面初始化路由任务队列
iterator 是执行任务队列的具体任务, 也就是每一项路由守卫及控制路由守卫执行操作
runQueue 的巧妙设计, 保证了守卫的中断机制, 使得路由守卫可以通过 next 来控制
6. `HistoryBase.updateRoute` 执行更新路由, 再触发全局 afterEach
```js
export function runQueue(queue, iter, end) {
updateRoute(route) {
const from = this.current;
this.current = route;
this.cb(this.current);
// 全局 afterEach 在 更新后执行的
this.router.afterHooks.forEach(hook => {
hook && hook(route, from);
});
}
}
```
将当前路由保留下来, 再将当前路由改为跳转的路由.
通知监听函数执行回调函数(cb).
全局 `afterEach` 在 更新后执行的
#### RouterView
```js
import Vue from "vue";
// 全局注册 RouterView
Vue.component("RouterView", RouterView);
```
#### RouterLink
```html
```
```js
import Vue from "vue";
Vue.component("RouterLink", RouterLink);
```