# 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); ```