简介
vue-router 是 Vue 官方提供的路由管理器, 和 Vue 的核心深度集成, 使得构建单页面变得容易。
由于 Ajax 技术的普及, 页面无刷新有更好的用户体验, 逐渐从单页面应用中的路由, 开始由后端走向前端。
路由流程
- 在 Vue 单页面体系中, ve-router 会有一个监听器, 用来监听浏览器 History 的变化。
- 通常情况下, 当浏览器地址栏的地址改变或点击游览器前进后退按钮, History 中的历史栈会相应地进行改变。
- 当监听到 History 变化后, vue-router 就会依据路由表中声明的路由匹配主键, 使用
routerView
组件进行渲染
相关问题
我们该如何监听游览器中的历史记录变化?
// 其一通过监听 window 上 popstate 事件来实现 HTML5Mode
window.addEventListener("popstate", () => {
console.log(window.location.pathname);
});
// 当url # 后的内容发生改变, 页面不刷新,会触发 onhashchange 函数来实现 HashMode
window.onhashchange = function() {
console.log(location.hash);
};
复制代码
回顾 vue-router 用法
基本用法
- 用于注册路由表, 并导出
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, //对应的组件
},
],
});
复制代码
- main.js 导入
new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
复制代码
动态路由
- 注册路由
export default new VueRouter({
//把我们的路由配置文件暴露出去
mode: "history", //history 默认是hash 标题带# 瞄点
routes: [
//路线 这个存放的是我们路由的配置文件
{
path: "/index/:id", //访问游览器的 路径
name: "index", // 这个是我们给路由起的名称
component: index, //对应的组件
},
],
});
复制代码
- 使用动态路由
<router-link to="/index/1">跳转到index 1 页</router-link>
<router-link to="/index/2">跳转到index 2 页</router-link>
复制代码
使用场景: 商品详情页面的时候,页面结构都一样,只是商品 id 的不同,所以这个时候就可以用动态路由动态。
动态路由就是在 path 路径后加 /:id
动态路由意味着组件实例会被复用, 生命周期钩子不会被重复调用
嵌套路由
- 注册路由
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 的 <router-view> 中
path: "profile",
component: Profile,
},
],
},
],
});
复制代码
- 实际应用界面,通常是多层嵌套的组件组合而成, 使用
children
来声明嵌套的路由组件
其一是通过监听 window 上 popstate 事件来实现 HTML5Mode
另一种是当 url # 后的内容发生改变, 页面不刷新,会触发 onhashchange 函数来实现 HashMode
路由守卫
导航守卫有三种, 全局路由守卫, 路由独享守卫, 组件守卫
全局守卫 | 路由独享守 | 组件内守卫 |
---|---|---|
beforeEach | beforeEnter | beforeRouteEnter |
beforeResolve | beforeRouteUpdate | |
afterEach | beforeRouteLeave |
实现思路
实现简易版 vue-router 需要至少分两条主线:
- 路由关系关联表 -- 监听器(两种模式) -- 渲染页面(render 相关组件)
- 导航守卫 -- 全局守卫 -- 路由独享守卫 -- 组件守卫
主线一
此条主线主要是从路由注册, 跳转方面进行讲解, 大致讲解了 Vue 单页面体系中路由管理器的基本实现
构建 Router 类, 创建关联路由表
- 构建 Router 类,
- 构造函数:
- 拿到关联好的路由表,
- 拿到 Html5Mode 实例, 并且将依赖注入,
- init 方法:
- 监听路由的改变,并重新赋值
- 执行页面的第一次跳转
- push 方法:
- 跳转页面
history.push
- 跳转页面
- 构造函数:
- 构建 RouterTable 类, 构建路由表 -- 构建路由的关联关系
- 构造函数:
- 创建 Map 用户缓存关联关系
- 初始化 routes
- init 方法:
- 遍历 routes, 将当前 route 添加(addRoute)到 pathMap 中
- 嵌套路由递归处理
- match 方法:
- 匹配当前 path 是否在_pathMap 当中
- 构造函数:
import Vue from "vue";
import RouterView from "./components/RouterView";
import RouterLink from "./components/RouterLink";
// 全局注册 RouterView 和 RouterLink
Vue.component("RouterView", RouterView);
Vue.component("RouterLink", RouterLink);
// 构建路由表 -- 构建路由的关联关系
class RouterTable {
constructor(routes) {
this._pathMap = new Map(); // 关联关系用map管理
this.init(routes); // 初始化routes
}
init(routes) {
// 将当前route添加到pathMap中
const addRoute = route => {
this._pathMap.set(route.path, route);
// 如果有嵌套路由
// if(route.children) {
// 对children forEach 处理嵌套
// }
};
// 遍历routes,对每个route进行addRoute操作
routes.forEach(route => addRoute(route));
}
// 匹配当前path是否在_pathMap当中
match(path) {
let find;
for (const key of this._pathMap.keys()) {
if (path === key) {
find = key;
break;
}
}
return this._pathMap.get(find);
}
}
import Html5Mode from "./history/html5";
// 构造路由的类
export default class Router {
constructor({ routes = [] }) {
this.routerTable = new RouterTable(routes); // 构建路由表
this.history = new Html5Mode(this); // 监听
}
init(app) {
const { history } = this; // 将history 解构出来
history.listen(route => { // 监听路由的改变,并重新赋值,会触发Vue.util.defineReactive 方法
app._route = route;
});
// 执行页面的第一次跳转
// history.transitionTo(history.getCurrentLocation());
}
push(to) {
this.history.push(to);
}
}
// 将自己的router 混入到 vue中
Router.install = function() {
Vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this); // 将vue 页面单应用注入
// 将自己的router 转为 响应式的
Vue.util.defineReactive(this, "_route", this._router.history.current);
} else {
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
});
};
复制代码
构建监听器
游览器模式分为 HashMode 和 Html5Mode 两种, 同时监听器也会分为两种
两种 model 建立 history 有相似和差异的地方,采用模板模式,构建一个基类
base 基类, 包括两种 Mode 都有的逻辑, hash 代表 hash mode, html5 代表 html5 mode
base 基类
- 构造函数中, 拿到派生类传递的 router 实例. 依赖反转, 将 routerTable 注入进来
- listen 方法:
- Vue.use(Router) 调用了 Router.install 方法
- Router.install 方法中将 vue 页面单应用注入(调用 init)
- 执行 Router 的 init 方法, 调用了 History.listen 方法, 传入回调函数 cb
- HistoryBase.listen 方法 接收到回调函数 cb, 进行保存
- 在路由发生改变时, 将当前的路由信息传递给回调函数 cb. 执行回调函数, 将路由注入到 Vue 单页面应用中.
- transitionTo 方法:
- 接收参数为跳转对应的路由, 在路由表中 match 找到对应的路由
- 执行更新路由
// 包括 hash model 和 html5 model 都有的逻辑
export default class HistoryBase {
constructor(router) {
this.router = router;
this.routerTable = router.routerTable; // 依赖反转,将routerTable注入进来
}
// 修改route的时机
listen(cb) {
this.cb = cb;
}
// 跳转对应的路由
transitionTo(target) {
// 判断 target 是否在路由表中
const route = this.routerTable.match(target);
// 当 路由守卫前置执行完成后, 执行更新路由,在render, 再触发全局 afterEach
this.updateRoute(route);
}
updateRoute(route) {
this.current = route;
this.cb(this.current);
}
}
复制代码
Html5Mode
- 构造函数: 接收构造时传入的参数, 并且传给基类。执行处理事件监听
- 事件监听处理方法(initListener): 通过监听
popstate
, 跳转到对应的路由 - 获取路由方法(getCurrentLocation): 返回当前完整的路由 path
- 路由跳转方法(push): 手动跳转路由, 并且需要手动向 popstate 添加一条记录
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 添加一条记录
}
}
复制代码
RouterView
- 获取当前路由, 没有路由就返回 404 页面
- 将当前路由进行解构,拿到 component 组件返回出去
- 将 RouterView 注册为全局组件
<script>
export default {
name: "RouterView",
render() {
// 获取当前的路由信息
const route = this._routerRoot._route;
// 没有匹配到路由的话就不会渲染, 如有必要可以渲染404页面
if (!route) {
return;
}
// 通过解构将route 上的component 取出来
const { component } = route;
return <component />;
},
};
</script>
复制代码
RouterLink
- 编写 RouterLink 组件, 添加点击事件
- 接收 props to 参数.
- 在点击事件中跳转到 to 路由
- 将 RouterLink 注册为全局组件
<template>
<a class="link" @click="jump">
<!-- 可以渲染标题 -->
<slot></slot>
</a>
</template>
<script>
export default {
name: "RouterLink",
props: {
to: {
type: String,
required: true
}
},
methods: {
jump() {
// 获取路由器
const router = this._routerRoot._router;
router.push(this.to);
}
}
};
</script>
<style scoped>
.link {
margin: 0 10px;
text-decoration: underline;
cursor: pointer;
}
</style>
复制代码
效果
主线二
首先需要理清在一些特定场景下, 会触发哪些导航守卫.
- bar 跳转到 /foo
- beforeRouteLeave bar 组件离开的守卫
- beforeEach 全局的前置守卫
- beforeRouteUpdate /根组件的改变守卫
- beforeEnter 路由独享守卫
- beforeRouteEnter foo 组件进入的守卫
- beforeResolve 全局的响应守卫
- afterEach 全局的后置守卫
- 页面初始化触发
- beforeEach 全局的前置守卫
- beforeEnter 独享路由守卫
- beforeRouteEnter 组件的前置守卫
- beforeResolve 全局的响应守卫
- afterEach 全局的后置守卫
使用
// 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;
复制代码
// foo.vue
<script>
export default {
// 组件路由解析前 -- 在路由独享守卫之后触发
beforeRouteEnter(to, from, next) {
console.log("foo::beforeRouteEnter");
next();
},
// 组件路由更改时解析
beforeRouteUpdate(to, from, next) {
console.log("foo::beforeRouteUpdate");
next();
},
// 组件路由离开时解析
beforeRouteLeave(to, from, next) {
console.log("foo::beforeRouteLeave");
next();
}
};
</script>
复制代码
分析:
全局路由守卫皆接收一个函数, 同时每个守卫可以出现多次, 此时需要一个队列进行收集.
收集全局路由守卫
// 构造路由的类
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
- Hash Mode
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 函数, 用于页面跳转
- Html5 Mode
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
指定的路由
实现首次跳转
Vue.use(Router)
将 router 注册, 同时触发Router.install
,将自己的 router 混入到 vue 中
// 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 注入
- 将当前路由信息转为响应式的
复制代码
router.init
函数用于监听路由的改变,并重新赋值, 执行首次跳转
export default class Router {
init(app) {
const { history } = this; // 将history 解构出来
history.listen((route) => {
// 监听路由的改变,并重新赋值,会触发Vue.util.defineReactive 方法
app._route = route;
});
// 执行页面的第一次跳转
history.transitionTo(history.getCurrentLocation());
}
}
复制代码
此函数在前面介绍过, 就不再累述.
HistoryBase.listen
监听函数, 用于保存 listen 回调函数
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);
});
}
}
复制代码
-
history.transitionTo
执行跳转函数, 接收参数为对应的路由- 通过
routerTable.match
函数可以拿到在路由表对应的路由信息 - 执行路由跳转的处理, 如页面初始化的路由守卫
- 当路由守卫执行完成后, 执行更新路由 updateRoute, 再触发全局 afterEach, 最后 render
- 通过
-
HistoryBase.confirmTransition
执行页面初始化路由守卫- 跳转页面是当前页面直接返回
- 将页面初始化用到的前置守卫组成一个队列
- 执行队列中所有的路由守卫
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 来控制
HistoryBase.updateRoute
执行更新路由, 再触发全局 afterEach
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
<script>
export default {
name: "RouterView",
render() {
// 获取当前的路由信息
const route = this._routerRoot._route;
// 没有匹配到路由的话就不会渲染, 如有必要可以渲染404页面
if (!route) {
return;
}
// 通过解构将route 上的component 取出来
const { component } = route;
// return <component />;
// 为了在组件守卫时 保证this指向,将vnode.componentInstance给到this
const hook = {
init(vnode) {
route.instance = vnode.componentInstance;
// console.log("vnode", vnode);
// console.log("instance", vnode.componentInstance);
},
};
return (<component hook={hook} />);
},
};
</script>
import Vue from "vue";
// 全局注册 RouterView
Vue.component("RouterView", RouterView);
复制代码
RouterLink
<template>
<a class="link" @click="jump">
<!-- 可以渲染标题 -->
<slot></slot>
</a>
</template>
<script>
export default {
props: {
to: {
type: String,
required: true,
},
},
methods: {
jump() {
// 获取路由器
const router = this._routerRoot._router;
router.push(this.to);
},
},
};
</script>
<style scoped>
.link {
margin: 0 10px;
text-decoration: underline;
cursor: pointer;
}
</style>
复制代码
import Vue from "vue";
Vue.component("RouterLink", RouterLink);
复制代码
效果
参考资料
标题 | 链接 |
---|---|
官方文档 | router.vuejs.org/zh/guide/ |
vue-router 源码 | github.com/vuejs/route… |
总结
本文从 vue-router
基础使用讲起, 从两大主线出发, 怎么实现简易版的 vue-router
展开描述
vue-router
只是一个框架, 本文的学习方法可以适用在很多框架中, 当然你也有更好的学习方法, 也欢迎从评论区说出来, 一道成长。
本文有兴趣可以移步个人gitee : gitee.com/wang_xi_lon…
我是前端小溪 欢迎感兴趣的同学关注下前端小溪公众号,也欢迎加我微信wxl-15153496335。