Vue使用者, 如何深入vue-router ?

简介

vue-router 是 Vue 官方提供的路由管理器, 和 Vue 的核心深度集成, 使得构建单页面变得容易。

由于 Ajax 技术的普及, 页面无刷新有更好的用户体验, 逐渐从单页面应用中的路由, 开始由后端走向前端。

路由流程

  1. 在 Vue 单页面体系中, ve-router 会有一个监听器, 用来监听浏览器 History 的变化。
  2. 通常情况下, 当浏览器地址栏的地址改变或点击游览器前进后退按钮, History 中的历史栈会相应地进行改变。
  3. 当监听到 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 用法

基本用法

  1. 用于注册路由表, 并导出
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, //对应的组件
    },
  ],
});
复制代码
  1. main.js 导入
new Vue({
  router,
  render: (h) => h(App),
}).$mount("#app");
复制代码

动态路由

  1. 注册路由
export default new VueRouter({
  //把我们的路由配置文件暴露出去
  mode: "history", //history 默认是hash 标题带# 瞄点
  routes: [
    //路线 这个存放的是我们路由的配置文件
    {
      path: "/index/:id", //访问游览器的 路径
      name: "index", // 这个是我们给路由起的名称
      component: index, //对应的组件
    },
  ],
});
复制代码
  1. 使用动态路由
<router-link to="/index/1">跳转到index 1 页</router-link>
<router-link to="/index/2">跳转到index 2 页</router-link>
复制代码

使用场景: 商品详情页面的时候,页面结构都一样,只是商品 id 的不同,所以这个时候就可以用动态路由动态。

动态路由就是在 path 路径后加 /:id

动态路由意味着组件实例会被复用, 生命周期钩子不会被重复调用

嵌套路由

  1. 注册路由
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,
        },
      ],
    },
  ],
});
复制代码
  1. 实际应用界面,通常是多层嵌套的组件组合而成, 使用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 当中
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 基类

  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 找到对应的路由
    • 执行更新路由
// 包括 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

  1. 构造函数: 接收构造时传入的参数, 并且传给基类。执行处理事件监听
  2. 事件监听处理方法(initListener): 通过监听popstate, 跳转到对应的路由
  3. 获取路由方法(getCurrentLocation): 返回当前完整的路由 path
  4. 路由跳转方法(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

  1. 获取当前路由, 没有路由就返回 404 页面
  2. 将当前路由进行解构,拿到 component 组件返回出去
  3. 将 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

  1. 编写 RouterLink 组件, 添加点击事件
  2. 接收 props to 参数.
  3. 在点击事件中跳转到 to 路由
  4. 将 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>
复制代码

效果

图片已删除

主线二

首先需要理清在一些特定场景下, 会触发哪些导航守卫.

  1. bar 跳转到 /foo
    • beforeRouteLeave bar 组件离开的守卫
    • beforeEach 全局的前置守卫
    • beforeRouteUpdate /根组件的改变守卫
    • beforeEnter 路由独享守卫
    • beforeRouteEnter foo 组件进入的守卫
    • beforeResolve 全局的响应守卫
    • afterEach 全局的后置守卫
  1. 页面初始化触发
    • 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 ModeHtml5 Mode

  1. 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 函数, 用于页面跳转

  1. 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 指定的路由

实现首次跳转

  1. 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 注入
    - 将当前路由信息转为响应式的

复制代码
  1. 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());
  }
}
复制代码

此函数在前面介绍过, 就不再累述.

  1. 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);
    });
  }
}
复制代码
  1. history.transitionTo 执行跳转函数, 接收参数为对应的路由

    • 通过routerTable.match 函数可以拿到在路由表对应的路由信息
    • 执行路由跳转的处理, 如页面初始化的路由守卫
    • 当路由守卫执行完成后, 执行更新路由 updateRoute, 再触发全局 afterEach, 最后 render
  2. 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 来控制

  1. 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。

猜你喜欢

转载自juejin.im/post/7085381244963258381
今日推荐