[关闭]
@wy 2020-06-04T17:54:55.000000Z 字数 3682 阅读 461

vue-router 导航守卫中 next 控制的实现

使用 vue-router 的导航守卫钩子函数,某些钩子函数可以让开发者根据业务逻辑,控制是否进行下一步,或者进入到指定的路由。

例如,后台管理页面,会在进入路由前,进行必要登录、权限判断,来决定去往哪个路由,以下是伪代码:

  1. // 全局导航守卫
  2. router.beforEach((to, from, next) => {
  3. if('no login'){
  4. next('/login')
  5. }else if('admin') {
  6. next('/admin')
  7. }else {
  8. next()
  9. }
  10. })
  11. // 路由配置钩子函数
  12. {
  13. path: '',
  14. component: component,
  15. beforeEnter: (to, from, next) => {
  16. next()
  17. }
  18. }
  19. // 组件中配置钩子函数
  20. {
  21. template: '',
  22. beforeRouteEnter(to, from, next) {
  23. next()
  24. }
  25. }

调用 next,意味着继续进行下面的流程;不调用,则直接终止,导致路由中设置的组件无法渲染,会出现页面一片空白的现象。

钩子函数有不同的作用,例如 beforEachafterEachbeforeEnterbeforeRouteEnterbeforeRouteUpdatebeforeRouteLeave,针对这些注册的钩子函数,要依次进行执行,并且在必要环节有控制权决定是否继续进入到下一个钩子函数中。

以下分析下源码中实现的方式,而源码中处理的边界情况比较多,需要抓住核心点,去掉冗余代码,精简出便于理解的实现。

精简源码核心功能

总结下核心点:钩子函数注册的回调函数,能顺序执行,同时会将控制权交给开发者。

先来一个能够注册回调函数的类:

  1. class VueRouter {
  2. constructor(){
  3. this.beforeHooks = []
  4. this.beforeEnterHooks = []
  5. this.afterHooks = []
  6. }
  7. beforEach(callback){
  8. return registerHook(this.beforeHooks, callback)
  9. }
  10. beforeEnter(callback){
  11. return registerHook(this.beforeEnterHooks, callback)
  12. }
  13. afterEach(callback){
  14. return registerHook(this.afterHooks, callback)
  15. }
  16. }
  17. function registerHook (list, fn) {
  18. list.push(fn)
  19. return () => {
  20. const i = list.indexOf(fn)
  21. if (i > -1) list.splice(i, 1)
  22. }
  23. }

声明的类,提供了 beforEachbeforeEnterafterEach 来注册必要的回调函数。

抽象出一个 registerHook 公共方法,作用:

  1. 注册回调函数
  2. 返回的函数,可以取消注册的回调函数

使用一下:

  1. const router = new VueRouter()
  2. const beforEach = router.beforEach((to, from, next) => {
  3. console.log('beforEach');
  4. next()
  5. })
  6. // 取消注册的函数
  7. beforEach()

以上的回调函数会被取消,意味着不会执行了。

  1. router.beforEach((to, from, next) => {
  2. console.log('beforEach');
  3. next()
  4. })
  5. router.beforeEnter((to, from, next) => {
  6. console.log('beforeEnter');
  7. next()
  8. })
  9. router.afterEach(() => {
  10. console.log('afterEach');
  11. })

以上注册的钩子函数会依次执行。beforEachbeforeEnter 的回调接收内部传来的参数,同时通过调用 next 可继续走下面的回调函数,如果不调用,则直接被终止了。
最后一个 afterEach 在上面的回调函数都执行后,才被执行,且不接收任何参数。

先来实现依次执行,这是最简单的方式,在类中增加 run 方法,手动调用:

  1. class VueRouter {
  2. // ... 其他省略,增加 run 函数
  3. run(){
  4. // 把需要依次执行的回调存放在一个队列中
  5. let queue = [].concat(
  6. this.beforeHooks,
  7. this.afterHooks
  8. )
  9. for(let i = 0; i < queue.length; i++){
  10. if(queue(i)) {
  11. queue(i)('to', 'from', () => {})
  12. }
  13. }
  14. }
  15. }
  16. // 手动调用
  17. router.run()

打印:

'beforEach'
'beforeEnter'

上面把要依次执行的回调函数聚合在一个队列中执行,并传入必要的参数,但这样开发者不能控制是否进行下一步,即便不执行 next 函数,依然会依次执行完队列的函数。

改进一下:

  1. class VueRouter {
  2. // ... 其他省略,增加 run 函数
  3. run(){
  4. // 把需要依次执行的回调存放在一个队列中
  5. let queue = [].concat(
  6. this.beforeHooks,
  7. this.afterHooks
  8. )
  9. queue[0]('to', 'from', () => {
  10. queue[1]('to', 'from', () => {
  11. console.log('调用结束');
  12. })
  13. })
  14. }
  15. }
  16. router.beforEach((to, from, next) => {
  17. console.log('beforEach');
  18. // next()
  19. })
  20. router.beforeEnter((to, from, next) => {
  21. console.log('beforeEnter');
  22. next()
  23. })

传入的 next 函数会有调用下一个回调函数的行为,把控制权交给了开发者,调用了 next 函数会继续执行下一个回调函数;不调用 next 函数,则终止了队列的执行,所以打印结果是:

'beforEach'

上面实现有个弊端,代码不够灵活,手动一个个调用,在真实场景中无法确定注册了多少个回调函数,所以需要继续抽象成一个功能更强的方法:

  1. function runQueue (queue, fn, cb) {
  2. const step = index => {
  3. // 队列执行结束了
  4. if (index >= queue.length) {
  5. cb()
  6. } else {
  7. // 队列有值
  8. if (queue[index]) {
  9. // 传入队列中回调,做一些必要的操作,第二个参数是为了进行下一个回调函数
  10. fn(queue[index], () => {
  11. step(index + 1)
  12. })
  13. } else {
  14. step(index + 1)
  15. }
  16. }
  17. }
  18. // 初次调用,从第一个开始
  19. step(0)
  20. }

runQueue 就是执行队列的通用方法。
1. 第一个参数为回调函数队列, 会依次取出来;
2. 第二个参数是函数,它接受队列中的函数,进行一些其他处理;并能进行下个回调函数的执行;
3. 第三个参数是队列执行结束后调用。

知道了这个函数的含义,来使用一下:

  1. class VueRouter {
  2. // ... 其他省略,增加 run 函数
  3. run(){
  4. // 把需要依次执行的回调存放在一个队列中
  5. let queue = [].concat(
  6. this.beforeHooks,
  7. this.beforeEnterHooks
  8. )
  9. // 接收回到函数,和进行下一个的执行函数
  10. const iterator = (hook, next) => {
  11. // 传给回调函数的参数,第三个参数是函数,交给开发者调用,调用后进行下一个
  12. hook('to', 'from', () => {
  13. console.log('执行下一个回调时,处理一些相关信息');
  14. next()
  15. })
  16. }
  17. runQueue(queue, iterator, () => {
  18. console.log('执行结束');
  19. // 执行 afterEach 中的回调函数
  20. this.afterHooks.forEach((fn) => {
  21. fn()
  22. })
  23. })
  24. }
  25. }
  26. // 注册
  27. router.beforEach((to, from, next) => {
  28. console.log('beforEach');
  29. next()
  30. })
  31. router.beforeEnter((to, from, next) => {
  32. console.log('beforeEnter');
  33. next()
  34. })
  35. router.afterEach(() => {
  36. console.log('afterEach');
  37. })
  38. router.run();

从上面代码可以看出来,每次把队列 queue 中的回调函数传给 iterator , 用 hook 接收,并调用。
传给 hook 必要的参数,尤其是第三个参数,开发者在注册的回调函数中调用,来控制进行下一步。
在队列执行完毕后,依次执行 afterHooks 的回调函数,不传入任何参数。

所以打印结果为:

  1. beforEach
  2. 执行下一个回调时,处理一些相关信息
  3. beforeEnter
  4. 执行下一个回调时,处理一些相关信息
  5. 执行结束
  6. afterEach

以上实现的非常巧妙,再看 Vue-router 源码这块的实现方式,相信你会豁然开朗。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注