【源码解析】VueRouter路由前置守卫,其实就是一个大筛子

1,315 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

前言

大家好,上一篇文章VueRouter构造函数中我们对VueRouter的构造函数进行了分析解读,了解到在创建VueRouter实例的同时还创建了路由的映射,定义了动态添加路由及获取路由的方法,处理了路由模式等等。接下来我们将一起来学习VueRouter为我们提供的一个路由导航守卫 - 全局前置守卫beforeEach。我们还是从它的用法开始。

全局前置守卫beforeEach

在router实例上有一个beforeEach方法,我们可以借助该方法来注册一个全局前置守卫,所谓的前置守卫可以理解为一个路由拦截器,也就是说所有的路由在跳转前都要先被前置守卫拦截,就像一个大筛子一样所有的路由都要过一遍筛子才能继续下去。这样我们就可以借助这个"筛子"做一些其它的操作,比如登录校验,在每次跳转到新页面前都要先检测一下用户有没有登录,或者有没有访问该页面的权限等,都可以在这个前置守卫中进行过滤。

前置守卫的使用也很简单,只需要调用路由实例上的beforeEach方法,并传递一个包含3个参数的回调函数即可,然后在回调函数中就可以做一些想做的事了。

router.beforeEach((to, from, next)=>{
    //...
});

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收三个参数:

  • to:是一个路由对象,表示要进入的路由
  • from:也是一个路由对象,表示当前要离开的路由(或者说从哪个路由过来的)
  • next:next是一个函数,在前置守卫中必须要调用这个方法,否则不会进入到新页面。该方法在调用时有如下几种传参方式
    • next(),直接调用,不传任何值,这种情况会直接进行管道中的下一个钩子
    • next(false),传递一个false,直接中断当前路由
    • next('/')或next({path:'/'}),跳转到对应的路由地址

源码解读

上面简单介绍了路由前置守卫的用法及作用,接下来我们解读一下它的源码,看看它是如何实现异步解析又是如何实现守卫的。首先我们先来看下下面的代码调试图及beforeEach的源码 caoshenhuinan (1).gif

VueRouter.prototype.beforeEach = function beforeEach (fn) {
    return registerHook(this.beforeHooks, fn)
};
  
function registerHook (list, fn) {
    list.push(fn);
    return function () {
      var i = list.indexOf(fn);
      if (i > -1) { list.splice(i, 1); }
    }
}

var queue = [].concat(
  ...
  this.router.beforeHooks,
  ...
);

runQueue(queue, iterator, function () {
  // wait until async components are resolved before
  // extracting in-component enter guards
  var enterGuards = extractEnterGuards(activated);
  var queue = enterGuards.concat(this$1.router.resolveHooks);
  runQueue(queue, iterator, function () {
    if (this$1.pending !== route) {
      return abort(createNavigationCancelledError(current, route))
    }
    this$1.pending = null;
    onComplete(route);
    if (this$1.router.app) {
      this$1.router.app.$nextTick(function () {
        handleRouteEntered(route);
      });
    }
  });
});

function runQueue (queue, fn, cb) {
    var step = function (index) {
      if (index >= queue.length) {
        cb();
      } else {
        if (queue[index]) {
          fn(queue[index], function () {
            step(index + 1);
          });
        } else {
          step(index + 1);
      }
    };
    step(0);
  }
  • 我们看到beforeEach的源码非常简单,仅仅调用了一个registerHook方法,并传递了this.beforeHooks和fn作为实参
    • beforeHooks是路由实例上的一个数组类型的属性
    • fn就是我们在调用beforeEach时传给它的包含to、from和next三个参数的回调函数
  • 再来看registerHook方法,该方法也很简单,其实最核心就做了一件事:就是将fn添加到beforeHooks数组中保存起来(该方法中的list就是从外面传进来的路由实例上的beforeHook)。
  • 然后registerHook方法的返回值也是一函数,在该函数中所做的事就是fn再从beforeHooks数组中删除出去 到这里其实关于beforeEach的源码就已经解读完毕了,但是我们好像解读了个寂寞,好像解读了也好像什么也没解读,到这里我们只知道befroeEach中的回调函数被添加到了beforeHooks数组中保存了起来,但是具体什么时候调用的,又是怎么调用的我们还一无所知,于是决定打开浏览器调试一番,别说还真有发现:原来这个beforeHooks会被添加到一个名为queue的队列中,然后再通过runQueue方法将队列中的方法一一执行。下面简单梳理下大概的执行流程
  • 首先在Vue调用init进行初始化时会调用beforeCreate钩子函数。前面我们在解读vuex的install方法时,不知道大家还记不记得在全局混入beforeCreate时,在根组件Vue的beforeCreate方法中会调用路由实例的init方法
  • 在路由实例的init方法中调用了transationTo方法,在该方法中又调用了confirmTransaction方法,最终在这里执行了runQueue方法,在前面我们提到这个方法会把队列queue中的所有函数调用执行,其中就包括beforeEach的回调函数fn
  • 那么进入到runQueue方法中我们看到:
    • runQueue接收三个参数:queue待执行的队列,fn 是外面定义的一个iterator函数,cb是一个回调函数
    • 在runQueue内部又定义并执行了step函数,第一次调用传递参数0
    • 在step中首先判断传进来的索引是否大于等于队列queue的长度,如果大于queue的长度则说明队列中的方法分已被执行完毕,这时就直接调用回调函数cb执行
    • 如果索引小于队列长度则将队列中对应索引的方法取出来并传给fn(前面定义的iterator)函数执行。注意这里只是让iterator函数执行,而不是真正的队列queue中的函数执行。至于队列queue中的方法最终是在这个iterator中被调用,进而也就执行到了beforeEach中的回调函数了

总结

本次我们分享了路由前置守卫的用法及用途,并对其实现原理进行了解读,简单概括为:

在beforeEach中调用了registerHook函数,并在该函数中将beforeEach的回调函数保存在路由实例的beforeHooks数组中。当根组件Vue的beforeCreate钩子函数执行时会调用到路由实例的init方法,在该方法中进行一系列调用后最终会执行到runQueue方法,然后通过该方法将队列中的方法一一调用执行

关于路由前置守卫的源码就分享到这里了,欢迎大佬们指点和补充!!!