第四章:穿透导航流程与守卫机制
在前几章中,我们深入探讨了 Vue Router 的本质、路由模式和路径匹配机制。现在,让我们深入研究 Vue Router 最核心的功能之一——导航流程与守卫机制。我们将学习导航守卫如何工作、异步流程如何控制,以及如何实现复杂的路由权限控制。
导航流程(Navigation Flow)
Vue Router 的导航流程是一个复杂但有序的过程,涉及多个步骤和守卫函数的执行。理解这个流程对于掌握 Vue Router 至关重要。
完整的导航解析流程
javascript
// 简化的导航流程实现
class NavigationFlow {
constructor(router) {
this.router = router
this.pendingLocation = null
}
// 导航到指定位置
async navigate(to, from, options = {}) {
// 1. 触发导航开始
const targetLocation = this.router.resolve(to)
// 如果是相同路由且参数相同,直接返回
if (isSameRouteLocation(from, targetLocation)) {
return
}
// 2. 设置待处理导航
this.pendingLocation = targetLocation
// 3. 执行导航守卫
try {
// 全局前置守卫
await this.runGuards(this.router.beforeEachGuards, targetLocation, from)
// 路由独享守卫
await this.runRouteGuards(targetLocation, from)
// 组件内守卫
await this.runComponentGuards(targetLocation, from)
// 全局解析守卫
await this.runGuards(this.router.beforeResolveGuards, targetLocation, from)
// 4. 确认导航
this.confirmNavigation(targetLocation, from, options)
// 5. 执行后置守卫
this.router.afterEachGuards.forEach(guard => {
guard(targetLocation, from)
})
// 6. 清除待处理导航
this.pendingLocation = null
return { redirected: false }
} catch (error) {
// 导航被取消或出错
this.pendingLocation = null
// 执行错误处理
this.router.errorHandlers.forEach(handler => {
handler(error)
})
throw error
}
}
// 执行守卫队列
async runGuards(guards, to, from) {
for (const guard of guards) {
const result = await new Promise((resolve, reject) => {
// 守卫函数可以接受 next 回调或返回 Promise
const next = (result) => {
if (result === false) {
// 中断导航
reject(new NavigationAborted())
} else if (typeof result === 'string' ||
(typeof result === 'object' && result.path)) {
// 重定向
reject(new NavigationRedirected(result))
} else {
// 继续导航
resolve(result)
}
}
// 执行守卫
const guardResult = guard(to, from, next)
// 如果守卫返回 Promise,则等待其完成
if (guardResult instanceof Promise) {
guardResult.then(next).catch(reject)
} else if (guardResult !== undefined) {
// 如果守卫同步返回结果
next(guardResult)
}
// 如果守卫没有返回任何内容且没有调用 next,等待其调用 next
})
if (result instanceof NavigationRedirected) {
throw result
}
}
}
// 执行路由独享守卫
async runRouteGuards(to, from) {
const matched = to.matched
for (const record of matched) {
if (record.beforeEnter) {
const guards = Array.isArray(record.beforeEnter)
? record.beforeEnter
: [record.beforeEnter]
await this.runGuards(guards, to, from)
}
}
}
// 执行组件内守卫
async runComponentGuards(to, from) {
// 这里简化处理,实际实现需要处理组件的异步加载等
const matched = to.matched
for (const record of matched) {
if (record.component && record.component.beforeRouteEnter) {
await new Promise((resolve, reject) => {
const next = (result) => {
if (result === false) {
reject(new NavigationAborted())
} else {
resolve(result)
}
}
record.component.beforeRouteEnter(to, from, next)
})
}
}
}
// 确认导航
confirmNavigation(to, from, options) {
// 更新当前路由
this.router.currentRoute.value = to
// 更新浏览器历史记录
if (!options.replace) {
this.router.history.push(to.path, {
...to,
from: from.path
})
} else {
this.router.history.replace(to.path, {
...to,
from: from.path
})
}
// 触发响应式更新
this.router.triggerReactivity()
}
}
// 导航异常类
class NavigationAborted extends Error {
constructor() {
super('Navigation aborted')
this.name = 'NavigationAborted'
}
}
class NavigationRedirected extends Error {
constructor(location) {
super('Navigation redirected')
this.name = 'NavigationRedirected'
this.location = location
}
}导航守卫(Guards)
导航守卫是 Vue Router 提供的强大功能,允许我们在路由导航过程中执行各种检查和操作。
beforeEach(to, from, next):全局前置守卫
全局前置守卫在每次导航之前执行,是实现权限控制和路由拦截的主要手段:
javascript
// 全局前置守卫实现
class Router {
constructor() {
this.beforeEachGuards = []
}
// 注册全局前置守卫
beforeEach(guard) {
this.beforeEachGuards.push(guard)
// 返回移除守卫的函数
return () => {
const index = this.beforeEachGuards.indexOf(guard)
if (index > -1) {
this.beforeEachGuards.splice(index, 1)
}
}
}
}
// 使用示例:权限控制
router.beforeEach((to, from, next) => {
// 检查路由是否需要认证
if (to.meta.requiresAuth) {
// 检查用户是否已登录
if (isAuthenticated()) {
next() // 允许导航
} else {
// 重定向到登录页
next('/login')
}
} else {
next() // 允许导航
}
})
// 使用示例:加载状态
router.beforeEach((to, from, next) => {
// 显示加载指示器
showLoadingIndicator()
next()
})
// 使用示例:日志记录
router.beforeEach((to, from, next) => {
console.log(`Navigating from ${from.path} to ${to.path}`)
next()
})异步守卫:如何支持 async 函数?
Vue Router 支持异步守卫,允许执行异步操作(如 API 调用):
javascript
// 异步守卫支持
router.beforeEach(async (to, from, next) => {
// 异步检查用户权限
try {
const user = await fetchUser()
if (user && hasPermission(to.meta.permission)) {
next()
} else {
next('/unauthorized')
}
} catch (error) {
console.error('Failed to check user permissions:', error)
next('/error')
}
})
// 使用 Promise 的守卫
router.beforeEach((to, from, next) => {
// 返回 Promise
return fetchUser()
.then(user => {
if (user) {
next()
} else {
next('/login')
}
})
.catch(error => {
next('/error')
})
})beforeResolve:在导航确认前执行
beforeResolve 守卫在所有组件内守卫和异步路由组件解析之后,导航被确认之前执行:
javascript
// beforeResolve 守卫
router.beforeResolve(async (to, from, next) => {
// 在这里可以执行需要确保所有组件都已解析的操作
try {
// 数据预取
await Promise.all(
to.matched
.filter(record => record.meta && record.meta.dataFetcher)
.map(record => record.meta.dataFetcher(to))
)
next()
} catch (error) {
console.error('Data fetching failed:', error)
next('/error')
}
})afterEach(to, from):全局后置钩子
全局后置钩子在导航完成后执行,不接受 next 函数也不会改变导航本身:
javascript
// 全局后置钩子
router.afterEach((to, from) => {
// 隐藏加载指示器
hideLoadingIndicator()
// 更新页面标题
if (to.meta.title) {
document.title = to.meta.title
}
// 发送页面浏览分析
sendPageView(to.path)
// 滚动到顶部
window.scrollTo(0, 0)
})组件内守卫
Vue Router 还提供了组件内的守卫函数:
javascript
// 组件内守卫示例
export default {
name: 'UserProfile',
// 在路由进入之前调用
async beforeRouteEnter(to, from, next) {
try {
// 注意:此时无法访问 this
const userData = await fetchUserData(to.params.id)
next(vm => {
// 通过回调访问组件实例
vm.userData = userData
})
} catch (error) {
next('/error')
}
},
// 在当前路由改变,但是该组件被复用时调用
async beforeRouteUpdate(to, from, next) {
// 可以访问 this
try {
this.userData = await fetchUserData(to.params.id)
next()
} catch (error) {
next('/error')
}
},
// 在导航离开该组件的对应路由时调用
beforeRouteLeave(to, from, next) {
// 可以访问 this
if (this.hasUnsavedChanges) {
const answer = window.confirm('您有未保存的更改,确定要离开吗?')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
}
}runQueue 与流程控制
Vue Router 内部使用 runQueue 函数来串行执行守卫函数,确保导航流程的正确性。
runQueue(guards, iterator, callback):串行执行守卫函数
javascript
// runQueue 实现
function runQueue(queue, fn, cb) {
const step = (index) => {
// 如果所有守卫都已执行完毕
if (index >= queue.length) {
cb()
} else {
// 执行当前守卫
fn(queue[index], () => {
// 递归执行下一个守卫
step(index + 1)
})
}
}
// 从第一个守卫开始执行
step(0)
}
// 使用 runQueue 执行守卫
function runGuards(guards, to, from, cb) {
runQueue(guards, (guard, next) => {
// 处理守卫执行
const guardResult = guard(to, from, next)
// 如果守卫返回 Promise
if (guardResult instanceof Promise) {
guardResult.then(next).catch(cb)
} else if (guardResult !== undefined) {
// 如果守卫同步返回结果
next()
}
// 如果守卫没有返回任何内容,等待其调用 next
}, cb)
}如何实现"任意守卫中断则停止后续执行"?
通过在 runQueue 中处理中断信号来实现:
javascript
// 支持中断的 runQueue 实现
function runQueueWithAbort(queue, fn, cb) {
let aborted = false
const step = (index) => {
// 如果已被中断,停止执行
if (aborted) {
cb(new Error('Navigation aborted'))
return
}
// 如果所有守卫都已执行完毕
if (index >= queue.length) {
cb()
} else {
// 执行当前守卫
fn(queue[index], (result) => {
// 检查是否需要中断
if (result === false) {
aborted = true
cb(new Error('Navigation aborted by guard'))
} else if (typeof result === 'string' ||
(typeof result === 'object' && result.path)) {
// 重定向
aborted = true
cb(new NavigationRedirected(result))
} else {
// 继续执行下一个守卫
step(index + 1)
}
})
}
}
step(0)
}错误处理:router.onError 如何捕获导航错误?
Vue Router 提供了错误处理机制来捕获导航过程中的错误:
javascript
// 错误处理实现
class Router {
constructor() {
this.errorHandlers = []
}
// 注册错误处理器
onError(handler) {
this.errorHandlers.push(handler)
return () => {
const index = this.errorHandlers.indexOf(handler)
if (index > -1) {
this.errorHandlers.splice(index, 1)
}
}
}
// 触发错误处理
handleError(error) {
this.errorHandlers.forEach(handler => {
try {
handler(error)
} catch (handlerError) {
console.error('Error in error handler:', handlerError)
}
})
}
}
// 使用示例
router.onError((error) => {
if (error.name === 'NavigationAborted') {
console.log('Navigation was aborted')
} else if (error.name === 'NavigationRedirected') {
console.log('Navigation was redirected to:', error.location)
} else {
console.error('Navigation error:', error)
// 显示错误页面
router.push('/error')
}
})小结
在本章中,我们深入探讨了 Vue Router 的导航流程与守卫机制:
- 导航流程 - 从导航开始到完成的完整过程
- 全局前置守卫 -
beforeEach实现权限控制和路由拦截 - 异步守卫支持 - 支持 Promise 和 async/await
- 解析守卫 -
beforeResolve在导航确认前执行 - 后置钩子 -
afterEach在导航完成后执行 - 组件内守卫 - 组件级别的路由控制
- 流程控制 -
runQueue实现守卫的串行执行 - 错误处理 -
router.onError捕获导航错误
这些机制共同构成了 Vue Router 强大的导航控制能力,使得开发者可以精确控制路由导航的各个方面。
在下一章中,我们将探讨 Vue Router 的生产级功能实现,包括 router-view 组件、router-link 组件、懒加载等。
思考题:
- 你在项目中如何使用导航守卫实现复杂的权限控制?遇到了哪些挑战?
- 对于异步守卫中的错误处理,你通常采用什么策略来确保用户体验?