路由模式

Hash 模式(默认)

  1. // onhashchange 事件触发条件:当一个窗口的 URL 中 # 后面的部分改变时
  2. window.onhashchange = function (event) {
  3. // http://localhost:8080/aaa/#/bbb -> http://localhost:8080/aaa/#/ccc
  4. console.log(event.oldURL, event.newURL);
  5. let hash = location.hash //通过 location 对象获取 hash 地址
  6. console.log(hash) // #/ccc
  7. }
  • 当 URL 改变时,页面不会重新加载;
  • Hash 变化的 URL 会被浏览器所记录,可以通过浏览器的“前进、后退”进行页面的切换和刷新;
  • onhashchange 中,仅能够改变 ‘#’ 之后的 url 片段;

    History 模式

  • http://localhost:8080/aaa/bbb

    原理:依靠浏览器提供的 History API 实现

  • hashchange 仅能够改变 ‘#’ 之后的 url 片段,而 history api 提供了完全的自由;

  • 具有浏览器历史记录,可以进行“前进”和“后退”的页面切换;
  • 当路径变化时,可以通过监听浏览器的路径变化;
  • History 模式可以进行“前进、后退”操作,但页面 F5 刷新时,就会报出页面 404;

    这时因为 F5 的页面刷新操作,是会真正到服务器请求页面的; 在 hash 模式下,前端路由修改的 # 部分,浏览器请求时不会携带,所以不会有问题; 但 history 模式下,服务器上没有相关的响应或资源时,会报错 404;

路由插件 install

在执行 Vue.use(Router) 之后,会调用 Router 插件的 install:

  1. // router.js
  2. import Vue from 'vue';
  3. import Router from './vue-router';
  4. Vue.use(Router);
  5. export default new Router({
  6. // 路由配置...
  7. })
  • vue-router 插件最终的导出结果是一个类 Router;
  • Router 实例化时,可以传入一个路由配置对象;
  • Router 类能够被 Vue.use(),说明 Router 类上包含了 install 方法; ```typescript // 导入路由安装逻辑 import install from “./install”;

class VueRouter { constructor(options) { // 传入路由配置对象 console.log(‘options >>’, options); } }

// 当 Vue.use 时,会自动执行插件上的 install VueRouter.install = install;

export default VueRouter;

  1. <a name="iKMtT"></a>
  2. ## install 逻辑实现
  3. - 插件安装时,指定插件依赖的 Vue 版本,并导出提供 vue-router 插件其他逻辑使用;
  4. - 在 Vue 全局上注册两个组件:<router-link><router-view>
  5. - 在 Vue 原型上添加两个属性:$router 和 $route;
  6. - $route:包含路由相关的属性,如 name、hash、meta 等
  7. - $router:路由相关的方法,如 history API (push,replace,go)
  8. - 通过 Vue.mixin 为每个组件混入根组件上的 router 实例
  9. ```typescript
  10. // 用于存储插件安装时传入的 Vue 并向外抛出,提供给插件中的其他文件使用
  11. // export 的特点:如果导出的值发生变化,外部会取得变化后的新值;
  12. export let _Vue;
  13. /**
  14. * 插件安装入口 install 逻辑
  15. * @param {*} Vue Vue 的构造函数
  16. * @param {*} options 插件的选项
  17. */
  18. export default function install(Vue, options) {
  19. _Vue = Vue; // 存储插件安装时使用的 Vue
  20. // 通过生命周期,为所有组件混入 router 属性
  21. Vue.mixin({
  22. beforeCreate() { // this 指向当前组件实例
  23. // 将 new Vue 时传入的 router 实例共享给所有子组件
  24. if (this.$options.router) { // 根组件才有 router
  25. this._routerRoot = this; // 为根组件添加 _routerRoot 属性指向根组件自己
  26. this._router = this.$options.router; // this._router 指向 this.$options.router
  27. } else { // 子组件
  28. // 如果是子组件,就去找父亲上的_routerRoot属性,并继续传递给儿子
  29. this._routerRoot = this.$parent && this.$parent._routerRoot;
  30. }
  31. // 这样,所有组件都能够通过 this._routerRoot._router 获取到同一个 router 实例;
  32. },
  33. })
  34. // 在 Vue 全局注册两个组件:`<router-link>` 和 `<router-view>`;
  35. Vue.component('router-link', {
  36. render: h => h('a', {}, '')
  37. });
  38. Vue.component('router-view', {
  39. render: h => h('div', {}, '')
  40. });
  41. // 在 Vue 原型上添加两个属性: `$router` 和 `$route`;
  42. Vue.prototype.$route = {};
  43. Vue.prototype.$router = {};
  44. }

路由映射表

路由插件初始化

  • 在 VueRouter 实例化时,接收外部传入的路由配置 options:new Router(options),构造函数内部通过 createMatcher 路由匹配器对其进行处理
  • 在路由匹配器 createMatcher 中,将路由配置(嵌套数组)处理为便于匹配的扁平化结构对象 matcher;
  • 在路由匹配器中创建路由插件的两个核心方法:match 和 addRoutes;
    • match:用于通过路由规则匹配到对应的组件
    • addRoutes:用于动态的添加路由匹配规则
  • 在 VueRouter 类中,创建路由初始化方法 init,当执行Vue.use安装路由插件在 install 方法中处理根组件时,执行路由的 init 初始化操作,将根实例 Vue.app 传入 init 方法中; ```typescript import createRouteMap from “./create-route-map”;

/**

  • 路由匹配器函数
  • 对路由配置进行扁平化处理
  • addRoutes:动态添加路由匹配规则
  • match:根据路径进行路由匹配
  • @param {*} routes
  • @returns 返回路由匹配器的两个核心方法 addRoutes、match */ export default function createMatcher(routes) { // 将嵌套数组的路由配置,处理为便于匹配的扁平结构 // 创建 match 方法:根据路径进行路由匹配 // 创建 addRoutes 方法:动态添加路由匹配规则

    // 路由配置的扁平化处理 let { pathMap } = createRouteMap(routes);

    // 根据路径进行路由匹配 function match(location) {

    1. let record = pathMap[location];
    2. return record;

    }

    /**

    • 动态添加路由匹配规则
    • 将追加的路由规则进行扁平化处理 */ function addRoutes(routes) { createRouteMap(routes,pathMap); }

      return { addRoutes, // 添加路由 match // 用于匹配路径 } } typescript /**

  • 路由配置扁平化处理
  • 支持初始化和追加两种情况
  • @param {*} routes 路由实例中的路由配置
  • @param {*} oldPathMap 路由规则映射表(扁平化结构)
  • @returns 新的路由规则映射表(扁平化结构) */ export default function createRouteMap(routes, oldPathMap) {

    // 拿到当前已有的映射关系 let pathMap = oldPathMap || Object.create(null);

    // 将路由配置 routes 依次加入到 pathMap 路由规则的扁平化映射表 routes.forEach(route => {

    1. addRouteRecord(route, pathMap);

    });

    return {

    1. pathMap

    } }

/**

  • 添加一个路由记录(递归当前的树形路由配置)
  • 先序深度遍历:先把当前路由放进去,再处理他的子路由
  • @param {*} route 原始路由记录
  • @param {*} pathMap 路由规则的扁平化映射表
  • @param {} parent 当前路由所属的父路由对象 /

function addRouteRecord(route, pathMap, parent) {

  1. // 处理子路由时,需要做路径拼接
  2. let path = parent ? (parent.path + '/' + route.path) : route.path;
  3. // 构造路由记录对象(还包含其他属性:path、component、parent、name、props、meta、redirect...)
  4. let record = {
  5. path,
  6. component: route.component,
  7. parent // 标识当前组件的父路由记录对象
  8. };
  9. // 查重:路由定义不能重复,否则仅第一个生效
  10. if (!pathMap[path]) {
  11. pathMap[path] = record;
  12. }
  13. // 递归处理当前路由的子路由
  14. if (route.children) {
  15. route.children.forEach(childRoute => {
  16. addRouteRecord(childRoute, pathMap, record);
  17. })
  18. }

}

  1. <a name="ZfOGm"></a>
  2. # 两种路由模式的设计
  3. 公共逻辑抽离到父类中实现:
  4. - transitionTo 根据路由进行匹配跳转
  5. ```typescript
  6. /**
  7. * 通过路由记录,逐层进行路由匹配
  8. * @param {*} record 路由记录
  9. * @param {*} location 路径
  10. * @returns 逐层匹配后的全部匹配结果
  11. */
  12. export function createRoute(record, location) {
  13. let res = []; //[/user /user/info]
  14. if (record) {
  15. while(record) {
  16. res.unshift(record);
  17. record = record.parent;
  18. }
  19. }
  20. return {
  21. ...location,
  22. matched: res
  23. }
  24. }
  25. class History {
  26. constructor(router) {
  27. this.router = router; // 存储子类传入的 router 实例
  28. // {'/': Record1, '/user': Record2, '/user/info': Record3 }
  29. this.current = createRoute(null, {
  30. path: '/'
  31. });
  32. }
  33. /**
  34. * 路由跳转方法:
  35. * 每次跳转时都需要知道 from 和 to
  36. * 响应式数据:当路径变化时,视图刷新
  37. * @param {*}} location
  38. * @param {*} onComplete
  39. */
  40. transitionTo(location, onComplete) {
  41. // 根据路径进行路由匹配
  42. let route = this.router.match(location);
  43. // 查重:如果前后两次路径相同,且路由匹配的结果也相同,那么本次无需进行任何操作
  44. if (location == this.current.path && route.matched.length == this.current.matched.length) { // 防止重复跳转
  45. return
  46. }
  47. // 使用当前路由route更新current,并执行其他回调
  48. this.updateRoute(route);
  49. onComplete && onComplete();
  50. }
  51. listen(cb) {
  52. // 存储路由变化时的更新回调函数,即 app._route = route;
  53. this.cb = cb;
  54. }
  55. /**
  56. * 路由变化时的相关操作:
  57. * 1,更新 current;
  58. * 2,触发_route的响应式更新;
  59. * @param {*} route 当前匹配到的路由结果
  60. */
  61. updateRoute(route) {
  62. // 每次路由切换时,都会更改current属性
  63. this.current = route;
  64. // 调用保存的更新回调,触发app._route的响应式更新
  65. this.cb && this.cb(route);
  66. }
  67. }
  68. export {
  69. History
  70. }
  • Hash 模式获取当前路径 hash 值
  • 监听 hashchange 事件 ```typescript import { History } from ‘./base’;

function ensureSlash() { // location.hash 存在兼容性问题,可根据完整 URL 判断是否包含’/‘ if (window.location.hash) { return; } window.location.hash = ‘/‘; // 如果当前路径没有hash,默认为 / }

class HashHistory extends History { constructor(router) { super(router); this.router = router; // Hash 模式下,对URL路径进行处理,确保包含’/‘ ensureSlash(); }

  1. getCurrentLocation() {
  2. // 获取路径的 hash 值
  3. return getHash();
  4. }
  5. setupListener() {
  6. // 当 hash 值变化时,获取新的 hash 值,并进行匹配跳转
  7. window.addEventListener('hashchange', () => {
  8. this.transitionTo(getHash());
  9. })
  10. }

} export default HashHistory

  1. - history 模式获取当前路径 path
  2. - 监听 popState 事件
  3. ```typescript
  4. import { History } from './base';
  5. class BrowserHistory extends History {
  6. constructor(router) {
  7. super(router); // 调用父类构造方法,并将 router 实例传给父类
  8. this.router = router; // 存储 router 实例,共内部使用
  9. }
  10. setupListener(){
  11. // 当路径变化时,拿到新的 hash 值,并进行匹配跳转
  12. window.addEventListener('popState',()=>{
  13. this.transitionTo(getHash());
  14. })
  15. }
  16. }
  17. export default BrowserHistory;

路由的响应式实现

  • 使用 Vue.util.defineReactive 将 this._router.history.current 定义为响应式数据

    1. // 通过生命周期,为所有组件混入 router 属性
    2. Vue.mixin({
    3. beforeCreate() { // this 指向当前组件实例
    4. // 将 new Vue 时传入的 router 实例共享给所有子组件
    5. if (this.$options.router) { // 根组件才有 router
    6. this._routerRoot = this; // 为根组件添加 _routerRoot 属性指向根组件自己
    7. this._router = this.$options.router; // this._router 指向 this.$options.router
    8. // 在根组件中,调用路由实例上的 init 方法,完成插件的初始化
    9. this._router.init(this); // this 为根实例
    10. // 目标:让 this._router.history.current 成为响应式数据;
    11. // 作用:current用于渲染时会进行依赖收集,当current更新时可以触发视图更新;
    12. // 方案:在根组件实例上定义响应式数据 _route,将this._router.history.current对象中的属性依次代理到 _route 上;
    13. // 优势:当current对象中的任何属性发生变化时,都会触发响应式更新;
    14. // Vue.util.defineReactive: Vue 构造函数中提供的工具方法,用于定义响应式数据
    15. Vue.util.defineReactive(this, '_route', this._router.history.current);
    16. } else { // 子组件
    17. // 如果是子组件,就去找父亲上的_routerRoot属性,并继续传递给儿子
    18. this._routerRoot = this.$parent && this.$parent._routerRoot;
    19. }
    20. // 这样,所有组件都能够通过 this._routerRoot._router 获取到同一个 router 实例;
    21. },
    22. })

    $route、$router 与 router-link 组件实现

  • 定义原型方法

    1. /**
    2. * 在 Vue 原型上添加 $route 属性 -> current 对象
    3. * $route:包含了路由相关的属性
    4. */
    5. Object.defineProperty(Vue.prototype, '$route', {
    6. get() {
    7. // this指向当前实例;所有实例上都可以拿到_routerRoot;
    8. // 所以,this._routerRoot._route 就是根实例上的 _router
    9. // 即:处理根实例时,定义的响应式数据 -> this.current 对象
    10. return this._routerRoot._route; // 包含:path、matched等路由相关属性
    11. }
    12. });
    13. /**
    14. * 在 Vue 原型上添加 $router 属性 -> router 实例
    15. * $router:包含了路由相关的方法
    16. */
    17. Object.defineProperty(Vue.prototype, '$router', {
    18. get() {
    19. // this._routerRoot._router 就是当前 router 实例;
    20. // router 实例中,包含 matcher、push、go、repace 等方法;
    21. return this._routerRoot._router;
    22. }
    23. });
  • 组件

    • 每次点击组件时,都会进行 hash 值的切换;
    • 组件可以接收来自外部的传参;
    • 组件渲染后返回的内容包括:元素标签 + 点击跳转事件 + 插槽等;
      1. export default {
      2. // 组件名称
      3. name: 'routerLink',
      4. // 接收来自外部传入的属性
      5. props: {
      6. to: { // 目标路径
      7. type: String,
      8. require: true
      9. },
      10. tag: { // 标签名,默认 a
      11. type: String,
      12. default: 'a'
      13. }
      14. },
      15. methods: {
      16. handler(to) {
      17. // 路由跳转:内部调用 history.push
      18. this.$router.push(to);
      19. }
      20. },
      21. render() {
      22. let { tag, to } = this;
      23. // JSX:标签 + 点击跳转事件 + 插槽
      24. return <tag onClick={this.handler.bind(this, to)}>{this.$slots.default}</tag>
      25. }
      26. }
  • this.$router.push 方法

    1. class VueRouter {
    2. push(to) {
    3. this.history.push(to); // 子类对应的push实现
    4. }
    5. }
    1. class HashHistory extends History {
    2. push(location) {
    3. // 跳转路径,并在跳转完成后更新 hash 值;
    4. // transitionTo内部会查重:hash 值变化虽会再次跳转,但不会更新current属性;
    5. this.transitionTo(location, () => {
    6. window.location.hash = location; // 更新 hash 值
    7. })
    8. }
    9. /**
    10. * 路由跳转方法:
    11. * 每次跳转时都需要知道 from 和 to
    12. * 响应式数据:当路径变化时,视图刷新
    13. * @param {*}} location
    14. * @param {*} onComplete
    15. */
    16. transitionTo(location, onComplete) {
    17. // 根据路径进行路由匹配;route :当前匹配结果
    18. let route = this.router.match(location);
    19. // 查重:如果前后两次路径相同,且路由匹配的结果也相同,那么本次无需进行任何操作
    20. if (location == this.current.path && route.matched.length == this.current.matched.length) {
    21. return;
    22. }
    23. // 使用当前路由route更新current,并执行其他回调
    24. this.updateRoute(route);
    25. onComplete && onComplete();
    26. }
    27. }

    router-view 组件

  • 获取渲染记录

  • 标记 router-view 层级深度,在当前组件向父组件追溯,进而确定当前层级
  • 根据深度进行 router-view 渲染 ```typescript

// 普通组件,使用组件需要先进行实例化再挂载:new Ctor().$mount(); // 函数式组件,无需创建实例即可直接使用(相当于 react 中的函数组件); // 他们之间唯一的区别就是 render 函数中没有 this,即没有组件状态(没有 data,props 等)

/**

  • router-view 函数式组件,多层级渲染
  • 函数式组件特点:性能高,无需创建实例,没有 this */ export default { name: ‘routerView’, functional: true, // true 表示函数式组件

    // 需要通过记录深度 depth 对应到每一层 router-view 要渲染的内容 render(h, { parent, data }) {

    1. // 获取当前需要渲染的相关路由记录,即:this.current
    2. let route = parent.$route;
    3. let depth = 0; // 记录等级深度
    4. // 在当前层级(第一层)的 data 属性中,添加自定义属性
    5. data.routerView = true;
    6. // App.vue渲染组件时,调用render函数,此时的父亲中没有 data.routerView 属性
    7. // 在渲染第一层时,添加routerView=true标识
    8. while(parent) { // parent 为 router-view 的父标签
    9. // parent.$vnode:代表占位符的vnode;即:组件标签名的虚拟节点;
    10. // parent._vnode 指组件的内容;即:实际要渲染的虚拟节点;
    11. if(parent.$vnote && parent.$vnote.data.routerView) {
    12. depth++;
    13. }
    14. parent = parent.$parent; // 更新父组件,用于循环的下一次处理
    15. }
    16. // 根据匹配结果与层级深度 depth,进行渲染
    17. // 第一层router-view 渲染第一个record 第二个router-view渲染第二个
    18. let record = route.matched[depth]; // 获取对应层级的记录
    19. // 未匹配到路由记录,渲染空虚拟节点(empty-vnode),也叫作注释节点
    20. if (!record) {
    21. return h();
    22. }
    23. // h(record.component):渲染当前组件,当组件渲染时,传入 data 数据,其中 data 包含了之前标识的 routerView 属性;
    24. return h(record.component, data);

    } } ```

    全局钩子函数

  • 钩子函数的订阅

发布订阅模式

  1. class VueRouter {
  2. constructor(options) { // 传入配置对象
  3. // 定义一个存放钩子函数的数组
  4. this.beforeHooks = [];
  5. }}
  6. // 在router.beforeEach时,依次执行注册的钩子函数
  7. beforeEach(fn){
  8. this.beforeHooks.push(fn);
  9. }
  10. }
  11. export default VueRouter;
  • 钩子的执行时机

eforeEach 钩子的执行时机:路由已经开始切换,但还没有更新之前

  • 执行注册的钩子函数 ```typescript /**
    • 递归执行钩子函数
    • @param {*} queue 钩子函数队列
    • @param {*} iterator 执行钩子函数的迭代器
    • @param {} cb 全部执行完成后调用 / function runQueue(queue, iterator, cb) { // 异步迭代 function step(index) { // 结束条件:队列全部执行完成,执行回调函数 cb 更新路由 if (index >= queue.length) return cb(); let hook = queue[index]; // 先执行第一个 将第二个hook执行的逻辑当做参数传入 iterator(hook, () => step(index + 1)); } step(0); }

class History { constructor(router) { this.router = router; }

/**

  • 路由跳转方法:
  • 每次跳转时都需要知道 from 和 to
  • 响应式数据:当路径变化时,视图刷新
  • @param {*}} location
  • @param {} onComplete / transitionTo(location, onComplete) { let route = this.router.match(location); if (location == this.current.path && route.matched.length == this.current.matched.length) { return } // 获取到注册的回调方法 let queue = [].concat(this.router.beforeHooks); const iterator = (hook, next) => { hook(this.current, route, () => { next(); }) } runQueue(queue, iterator, () => { // 将最后的两步骤放到回调中,确保执行顺序 // 1,使用当前路由route更新current,并执行其他回调 this.updateRoute(route); // 根据路径加载不同的组件 this.router.matcher.match(location) 组件 // 2,渲染组件 onComplete && onComplete(); }) } }

export { History } ```

参考资料

  1. 【VueRouter 源码学习】第一篇 - 环境搭建与路由模式介绍