1、内容简介
啊就是这样这样然后那样那样,vue-router就把导航守卫处理往啦~ 怎么样,是不是很急速!是不是很省流!
开个玩笑 开个玩笑 盆友留步
本文将介绍一下几个内容:
- vue-router中导航守卫相关的源码实现
- 对于路径切换问题的通用解决方案
- 迭代器的设计模式,如何依次执行一个异步队列(依次发起接口请求)
单看源码干巴巴的,所以这次尽量少些源码,多画点图!
附意识流笔记一份: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实例中的某个属性,直接取值。
但组件级的守卫则比较复杂,我们重点分析下这两个函数:extractLeaveGuards
、extractUpdateHooks
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
))
}))
}
这这这这,咋老母族戴胸罩一套又一套的。。。
先别急,我来翻译翻译
它的实现大概是这样的
其实就是
- 拿到所有匹配的路由
- 依次获取每个路由下的每个组件
- 从每个组件中取得对应名称的导航守卫
那现在守卫的队列有了,就剩下执行了
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、总结
简单来说,对于导航守卫的处理大致就是:
-
利用路径比对的算法,得出更新、激活、失活三个类型的路由数组
-
对于全局、路由级别的守卫,直接从指定属性中获取守卫列表
-
对于组件级别的守卫则:
3.1. 遍历每个类型的路由数组,取得其下的组件
3.2. 从组件中取得对应名称的导航守卫
-
构建异步队列并依次执行