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

相关推荐
孜然卷k4 分钟前
前端导出word文件,并包含导出Echarts图表等
前端·javascript
家里有只小肥猫25 分钟前
uniApp小程序保存canvas图片
前端·小程序·uni-app
前端大全28 分钟前
Chrome 推出全新的 DOM API,彻底革新 DOM 操作!
前端·chrome
八角丶39 分钟前
元素尺寸的获取方式及区别
前端·javascript·html
冴羽1 小时前
Svelte 最新中文文档教程(16)—— Context(上下文)
前端·javascript·svelte
前端小臻1 小时前
关于css中bfc的理解
前端·css·bfc
白嫖不白嫖1 小时前
网页版的俄罗斯方块
前端·javascript·css
HappyAcmen1 小时前
关于Flutter前端面试题及其答案解析
前端·flutter
顾比魁1 小时前
pikachu之CSRF防御:给你的请求加上“网络身份证”
前端·网络·网络安全·csrf
林的快手1 小时前
CSS文本属性
前端·javascript·css·chrome·node.js·css3·html5