省流,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. 构建异步队列并依次执行

相关推荐
吕彬-前端11 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱13 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai23 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb