vue-router導航守衛
在本期文章中,我將為大家梳理弄明白以下幾個事情,
1:導航守衛的執行順序是怎麼樣的?
2:導航守衛中的next的用處?
3:為什麼afterEach守衛沒有next?
4:beforeEach是否可以疊加?
5:路由跳轉經歷了哪幾部分?
在之前說過的一個內容router例項的history屬性幫助我們做了所有跳轉部分的事情,所以導航守衛的內容也在history中。
我們以HTML5History這個類來看一下這個push方法,push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
複製程式碼
push(我們跳轉時的$router.push就是這個方法)過程中呼叫了transitionTo完成了一系列的跳轉內容,但這個方法在HTML5的類中並不存在,繼承於base.js類中的方法 transitionTo就是實現路由跳轉的方法 transitionTo的主流程是由confirmTranstion方法於uodateRoute方法結合起來的,翻譯成普通話:路由跳轉要先經過一個確認跳轉的過程,在確認過程完成後進行一次路由的更新操作,
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 獲取要跳轉的並且經過處理的路由
const route = this.router.match(location, this.current)
// confirmTranstion確認跳轉過程
this.confirmTransition(route, () => {
// 確認完畢後完成更新路由操作
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
複製程式碼
confirmTransiton做了什麼呢?首先判斷一下你是不是相同的路由。如果是那就什麼都不做,第二步呢,我們要開始收集一波守衛了,然後把守衛收集起來,然後把每個守衛執行一遍,confirmTransition就算執行成功了。
下面是部分原始碼截圖:
這個過程中的難點是什麼?
如何收集守衛組合成守衛佇列?
如何按順序執行守衛的同時可以隨時中止守衛佇列?
如何找到守衛佇列執行完畢後的那個節點(守衛佇列執行完可以通知一下)
在vue-router中封裝了一個runQueue函式來解決上面的三個問題的後兩個。第一個問題呢則涉及到vue-router處理路由的一個大篇章,我們著重講一下runQueue函式
runQueue函式的思想:
1:迭代器模式來保證遍歷佇列時每一步都是可控的,
2:佇列完成後執行對應的回撥函式,
推斷出函式引數的對應功能:
queue : 需要執行的守衛佇列
fn : 迭代器函式,守衛佇列的每一個守衛都去執行迭代器函式
fn的第二個引數使迭代器進入下一步,不掉用就不會進入下一步(很重點)
cb : 結束時呼叫的回撥函式
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)
}
複製程式碼
runQueue是怎麼幫助我們解決守衛佇列處理的問題就算說完了。
(快留起來,這函式簡直吊極了!)
處理守衛佇列的大錘子我們已經制造好了,可以開工了,那你的守衛佇列呢??
對對對,還有守衛佇列要收集。 這個時候我們要想想有哪些守衛?
守衛有兩大種類:前置守衛、後置守衛。
- 前置守衛:
-
全域性的前置守衛: beforeEach beforeResolve
-
路由獨享的守衛: beforeEnter
-
元件內的守衛: beforeRouterEnter、beforeRouterUpdate、beforeRouteLeave
-
- 後置守衛:
- 全域性的後置守衛: afterEach
我們要想一下這些守衛都是怎麼註冊的,
-
在路由例項註冊的:
beforeEach、beforeResolve、afterEach
-
在路由配置中註冊的(路由獨享守衛):
beforeEnter
-
元件內的路由守衛:
beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter
好了我們要去榨取對應的守衛了,
confirmTransition的守衛分為兩個佇列:我們先來看第一個佇列
// 拿到路由跳轉中更新、摧毀、啟用時對應展示的元件。
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
// 路由守衛
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)
複製程式碼
一個queue的順序:
- 拿到被摧毀的元件的,榨取出所有元件內的離開守衛。
- 全域性的beforeEach元件。
- 拿到更新的所有元件,榨取出所有元件內的更新守衛。
- 遍歷要進入的路由,拿到所有路由的獨享守衛。
- 載入要被啟用的非同步元件
7個守衛中的4個守衛都在被按順序拿出來了,放入第一個queue。
再下一步要有一個處理守衛的迭代器:
我們該如何處理守衛?
- 保證在守衛中可以停止並且跳轉到其餘路由,
- 保證守衛可以正常通過,
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
// 傳個false就直接執行路由的錯誤處理,然後停止什麼都不做。
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
// 如果我們接受了一個可以操作的路徑。
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
// 我們就執行路由跳轉操作,並且守衛佇列停止下面的迭代
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
// 接續迭代下去咯
next(to)
}
})
} catch (e) {
abort(e)
}
}
複製程式碼
next函式,之前在將runQueue的函式的時候,fn接收第二個引數(之前畫過重點),第二個引數的回撥函式是完成迭代器向下一步執行的功能。
下面會有一點亂:
所有的前置守衛都接收三個引數
beforeEnter(to,from,next)=>{
//這個next就是我們看到的 hook裡面接收的箭頭函式((to:any)=>{})
//這個箭頭函式裡面對迭代器的next進行了一下掉用,
//保證在一定情況下迭代器可以向下走一步。
next('/index')
// 我們在這種next('/index')傳遞一個可以執行的路徑時,(to:any)=>{}
//這個箭頭函式並不會呼叫迭代的next,而是跳轉別的路徑執行了push操作。
// 如果我們不掉用守衛中的next,迭代器的next肯定並不會執行,守衛的迭代就停止了,
// 守衛堵塞confirmTransition並不會執行完畢,也就不會由後面的更細路由操作了。
}
複製程式碼
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
})
})
複製程式碼
我們在把第一個queue(四個守衛與一個非同步元件的載入)執行完畢後,要收集與執行第二個queue了,
第二個queue:
- 收集了被的啟用元件內的進入守衛
- 全域性的beforeResolve的守衛
收集完開始執行第二個queue的迭代。第二個queue執行完執行一下onComplete函式,代表著confirmTransition方法執行完畢了。確認路由的過程結束了,
下面就是updateRoute的過程。updateRoute的時候執行全部的後置守衛,因為更新路由之後,當前的路由已經變化了,所以在給守衛傳引數的時候快取了一下,之前的路由。
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
複製程式碼
所以為什麼afterEach沒有next呢?因為afterEach根本不在迭代器之內,他就沒有next來觸發迭代器的下一步。
最後我們說一下beforeEach的內容: 我們設定beforeEach全域性守衛的時候,守衛們儲存在哪裡?
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
function registerHook (list: Array<any>, fn: Function): Function {
list.push(fn)
// 返回值是一個function
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
複製程式碼
這段程式碼beforeEach是通過註冊守衛的方式,將註冊的全域性前置守衛放在beforeHooks的容器內,這個容器裡面裝載著所有的前置守衛
一家人(全域性的 前置進入、前置resolve、後置守衛)整整齊齊的放在對應的容器裡面,容器是個陣列,所以註冊全域性守衛的時候,是支援註冊多個的,router.beforeEach(()=>{xxx});
router.beforeEach(()=>{yyy});
// 這兩個守衛都會執行,只是先註冊的先執行,
// registerHook這個方法還可以清除對應的守衛,這個方法也可以使用
複製程式碼
總結
我們來回答一下開篇的5個問題
1:導航守衛的執行順序是怎麼樣的?
beforeRouteLeave < beforeEach < beforeRouteUpdate < beforeEnter < beforeRouteEnter < beforeResolve < afterEach
2:導航守衛中的next的用處?
next的作用,使導航守衛佇列的繼續向下迭代
3:為什麼afterEach守衛沒有next?
afterEach根本不在導航守衛佇列內,沒有迭代的next
4:beforeEach是否可以疊加?
beforeEach是可以疊加的,所有的全域性前置守衛按順序存放在beforeHooks的陣列裡面,
5:路由跳轉經歷了哪幾部分?
路由跳轉的核心方法是transitionTo,在跳轉過程中經歷了一次confirmTransition,
(beforeRouteLeave < beforeEach < beforeRouteUpdate < beforeEnter < 非同步元件載入)這樣順序的queue為第一個,
在第一個queue迭代完畢後,執行第二個(beforeRouteEnter < beforeResolve)這樣順序的queue,
在執行完畢後,開始執行updateRoute,之後執行全域性的afterEach守衛。最後完成路由的跳轉。