省流,vue-router是这样处理导航守卫的

1、内容简介

啊就是这样这样然后那样那样,vue-router就把导航守卫处理往啦~ 怎么样,是不是很急速!是不是很省流!

开个玩笑 开个玩笑 盆友留步

本文将介绍一下几个内容:

  1. vue-router中导航守卫相关的源码实现
  2. 对于路径切换问题的通用解决方案
  3. 迭代器的设计模式,如何依次执行一个异步队列(依次发起接口请求)

单看源码干巴巴的,所以这次尽量少些源码,多画点图!

附意识流笔记一份:Vue Router -- 路径切换

2、导航守卫

vue-router支持了很多导航守卫,如全局级的守卫:

js 复制代码
const router = new VueRouter({ ... })

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

路由级的守卫

js 复制代码
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件级的守卫

js 复制代码
const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
}

我们知道,这些守卫都会在浏览器路由切换的时候,相继被调用;

但vue-router是怎么管理这些守卫的呢?

这些守卫的执行顺序与执行机制又是怎样的呢?

3、切换路由的方式

当我们使用vue-router的时候,可以通过以下几种方式去更换浏览器的链接:

1、使用router-link

js 复制代码
 <router-link to="/foo">Go to Foo</router-link>
 <router-link to="/bar">Go to Bar</router-link>

2、点击浏览器的前进或后退

3、router的api

js 复制代码
router.push('home')

router.replace('home')

router.go(1)

这几种方式都会走到history对象的transitionTo方法中,即:router.history.transitionTo

4、history对象

在new调用vue-router的构造函数的时候,该构造函数就会为router实例初始化一个history实例

js 复制代码
export default class VueRouter {
  constructor (options: RouterOptions = {}) {
    // ...一些逻辑
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }

这个history就包含了路由管理等相关功能,且会按用户选择的模式(hash、history)初始化不同类型。

这些不同类型的history构造函数都是基于History这个构造函数继承而来的,而前面我们提到的transitionTo方法的实现也是在History中,它的实现大致如下:

js 复制代码
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    // ...做了一些事情
    this.confirmTransition(
      route,
      () => {
          // 成功的回调
      },
      err => {
          // 失败的回调
      }
    )
  }

对于路径的匹配、导航守卫等的逻辑,都在confirmTransition方法中,下面我们来重点分析这个方法。

5、confirmTransition

简单来说,这个方法一共做了下面几件事情

1、检查传入的路由是否是一个新路由,不是则拒绝处理

js 复制代码
if (
      isSameRoute(route, current) &&
      // ...其他一些判断逻辑
    ) {
  // ... 一些处理
  return abort(createNavigationDuplicatedError(current, route))
}

2、对比新旧路由,算出哪些路由的应该更新的(update)、哪些是应该激活的(activate)、哪些是应该失活的(deactivate)

js 复制代码
const { updated, deactivated, activated } = resolveQueue(
  this.current.matched,
  route.matched
)

这里其实涵盖了一个对路径切换问题的通用解决方案,我们稍后再做分析。

先看这三个数据的元素,它们都是RouteRecord的实例,且是根据传入的路由配置构建的;它的components属性就是路由配置的component,大概是这个样子:

js 复制代码
const record: RouteRecord = {
    path: normalizedPath, // `${parent.path}/${path}`
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component }, // 路由的组件
    instances: {}, // 组件的实例
    // ...其他一些属性
    beforeEnter: route.beforeEnter, // 路由级的守卫
  }

3、构建涵盖各个导航守卫的异步队列

js 复制代码
const queue: Array<?NavigationGuard> = [].concat(
  extractLeaveGuards(deactivated), // 失活的路由的守卫
  this.router.beforeHooks, // 全局的路由守卫
  extractUpdateHooks(updated), // 更新的路由的守卫
  activated.map(m => m.beforeEnter), // 激活的路由的守卫
  resolveAsyncComponents(activated) // 处理新路由的组件
)

4、构建一个迭代器去按顺序执行这个异步队列

js 复制代码
const iterator = (hook: NavigationGuard, next) => {
    // ...做了一些事情
}

// 按顺序执行异步队列
runQueue(queue, iterator, () => {
    // ...执行完成的回调
})

具体的实现我们一个来分析。

(ps:第一点是不是新路由的判断方法就不看了,反正就是这样这样,然后那样那样就判断出来啦~)

6、 路径对比

首先,vue-router对于路由配置如:

js 复制代码
const routes = [
  { path: '/foo', component: Foo, children: [...] },
  { path: '/bar', component: Bar }
]

会将其转换为一个RouteRecord实例(先不用管具体的实现),RouteRecord实例有children属性记录子节点,有parent属性记录父节点,所以一整个路由配置可以看成一个树:

而当我们从路由E切换到路由F的时候,因为RouteRecord实例带有parent指针,我们很容易可以往上溯源,找到新旧两个路径:

js 复制代码
旧:
    A -> C -> E
新:
    A -> C -> D -> F

这个时候,我们就可以定义一个指针,来一步一步找到这两个路径第一个不同的位置,即E的位置:

那接下来就很简单了

这个是vue-router对于路径切换问题的解决方案,对于相识的场景,我们是可以参考他的实现的。

比如说,当我从下图的三级1-1-2,切换到 三级1-1-1的时候,需要去更新对应节点的check状态

虽然在路径变更前去遍历一整棵树,先将check状态置为false,再将新路径中的check置为ture;这种方案可行,但就是不太优雅,时间复杂度也更高。

而我们只需要和vue-router一样,对比新旧两条路径,就可以很快算出需要取消选中的节点(三级1-1-2), 和需要选中的节点(三级1-1-1)

7、 构建导航守卫队列

好,我们言归正传回到vue-router源码,在得到updated, deactivated, activated这三个数组以后,我们就要去这三个数组里面拿对应的导航守卫了。

js 复制代码
// 由导航守卫啥的构成的异步队列
const queue: Array<?NavigationGuard> = [].concat(
  extractLeaveGuards(deactivated), // 失活的路由的守卫
  this.router.beforeHooks, // 全局的路由守卫
  extractUpdateHooks(updated), // 更新的路由的守卫
  activated.map(m => m.beforeEnter), // 激活的路由的守卫
  resolveAsyncComponents(activated) // 处理新路由的组件
)

对于全局级的、路由级的守卫的获取,都比较简单;就是从router实例或是route实例中的某个属性,直接取值。

但组件级的守卫则比较复杂,我们重点分析下这两个函数:extractLeaveGuardsextractUpdateHooks

js 复制代码
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
  return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

function extractGuards (
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  const guards = flatMapComponents(records, (def, instance, match, key) => {
    // 做了一些处理
  })
  return flatten(reverse ? guards.reverse() : guards)
}

看起来,是调用了flatMapComponents,还得进去看看它是怎么实现的

js 复制代码
export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Funct rn flatten(matched.map(m => {
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}

这这这这,咋老母族戴胸罩一套又一套的。。。

先别急,我来翻译翻译

它的实现大概是这样的

其实就是

  1. 拿到所有匹配的路由
  2. 依次获取每个路由下的每个组件
  3. 从每个组件中取得对应名称的导航守卫

那现在守卫的队列有了,就剩下执行了

8、 异步队列依次执行的函数

这里的实现也比较简单,先看代码

js 复制代码
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

构建一个step函数,它的每次执行都会取到队列中的下一个函数去执行。

而在执行队列中函数的时候,则会用一个next函数将step包裹起来,从而将迭代的时机,交给了队列的元素。

这就是典型的外部迭代器的设计模式。

但对于这种需要依次执行异步队列的场景(如依次调用接口),我个人是比较喜欢用promise:

js 复制代码
const loader1 = () => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(1)
            resolve()
        }, 1000)
    })
}
const loader2 = () => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(2)
            resolve()
        }, 500)
    })
}
const loader3 = () => {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(3)
            resolve()
        }, 200)
    })
}
async function runQueue(queue) {
    queue.reduce((preP, curLoader) => {
        return preP.then(curLoader)
    }, Promise.resolve())
}
runQueue([loader1, loader2, loader3])

换成es6的实现方案则是

js 复制代码
async function runQueue(queue) {
    for (let loader of queue) {
        await loader()
    }
}
runQueue([loader1, loader2, loader3])

其好处就是,实现更加的规范;异步函数通过控制其返回的promise解决实际,来控制队列中下一个函数何时被执行。

9、总结

简单来说,对于导航守卫的处理大致就是:

  1. 利用路径比对的算法,得出更新、激活、失活三个类型的路由数组

  2. 对于全局、路由级别的守卫,直接从指定属性中获取守卫列表

  3. 对于组件级别的守卫则:

    3.1. 遍历每个类型的路由数组,取得其下的组件

    3.2. 从组件中取得对应名称的导航守卫

  4. 构建异步队列并依次执行

相关推荐
excel3 分钟前
前端必备:从能力检测到 UA-CH,浏览器客户端检测的完整指南
前端
前端小巷子10 分钟前
Vue 3全面提速剖析
前端·vue.js·面试
悟空聊架构17 分钟前
我的网站被攻击了,被干掉了 120G 流量,还在持续攻击中...
java·前端·架构
CodeSheep18 分钟前
国内 IT 公司时薪排行榜。
前端·后端·程序员
尖椒土豆sss22 分钟前
踩坑vue项目中使用 iframe 嵌套子系统无法登录,不报错问题!
前端·vue.js
遗悲风23 分钟前
html二次作业
前端·html
江城开朗的豌豆26 分钟前
React输入框优化:如何精准获取用户输入完成后的最终值?
前端·javascript·全栈
CF14年老兵26 分钟前
从卡顿到飞驰:我是如何用WebAssembly引爆React性能的
前端·react.js·trae
画月的亮29 分钟前
前端处理导出PDF。Vue导出pdf
前端·vue.js·pdf
江城开朗的豌豆35 分钟前
拆解Redux:从零手写一个状态管理器,彻底搞懂它的魔法!
前端·javascript·react.js