Vue Router 导航守卫:一场路由冒险记
各位靓仔靓女们,晚上好!我是今晚的讲师,咱们来聊聊 Vue Router 里那些神出鬼没的“导航守卫”。它们就像路由世界的保安,负责检查你的通行证,决定你能不能去想去的地方。
准备好了吗?系好安全带,我们开始这场路由冒险记!
1. 导航守卫:路由世界的保安
Vue Router 提供了三种全局导航守卫:
beforeEach
: 路由跳转前置守卫,路由卫兵。beforeResolve
: 路由解析守卫,路由最后的检查员。afterEach
: 路由跳转后置守卫,路由记录员。
它们都是函数,接收三个参数:
to
: 即将要进入的目标路由对象。from
: 当前导航正要离开的路由对象。next
: 这是一个函数,控制导航的进行。
2. next
函数:路由冒险的通行证
next
函数是导航守卫的核心,它决定了路由跳转的命运。它可以接受以下参数:
next()
: 允许导航继续。next(false)
: 中断当前的导航。next(path)
: 重定向到不同的路由。path
可以是字符串或路由对象。next(error)
: 中断导航,并将错误传递给router.onError()
注册的回调。
3. 导航守卫的执行顺序:一场精心策划的路由冒险
导航守卫的执行顺序非常重要,它决定了路由跳转过程中发生的事情。简单的说:
beforeEach
: 全局前置守卫,就像在进入路由世界前,必须先经过安检,检查你的身份。- 路由组件内的
beforeRouteLeave
守卫: 离开当前路由组件前,组件内的守卫会检查你是否允许离开。 - 路由配置中的
beforeEnter
守卫: 进入目标路由前,路由配置中的守卫会检查你是否允许进入。 - 异步路由组件解析: 如果路由组件是异步的,Vue Router 会在这里解析它们。
- 路由组件内的
beforeRouteUpdate
守卫: 如果当前路由组件被复用 (比如参数变化),组件内的守卫会被调用。 beforeResolve
: 全局解析守卫,就像在最终确认进入路由前,再进行一次检查。- 导航被确认: Vue Router 更新 DOM,渲染新的组件。
- 路由组件内的
beforeRouteEnter
守卫: 进入目标路由组件时,组件内的守卫会被调用。 afterEach
: 全局后置守卫,就像在路由跳转完成后,记录你的行踪。
可以用一个表格来更清晰地展示:
阶段 | 守卫 | 作用 |
---|---|---|
准备阶段 | 全局 beforeEach |
在每次路由导航前被调用,用于检查用户身份验证、权限控制等。 |
离开阶段 | 组件内 beforeRouteLeave (即将离开的组件) |
在离开当前组件的对应路由时被调用,用于阻止用户离开,例如,提示用户保存未保存的更改。 |
进入阶段 | 路由独享 beforeEnter |
在进入特定路由时被调用,用于验证用户是否具有访问该路由的权限。 |
解析阶段 | 异步路由组件解析 | Vue Router 会在这里解析异步路由组件。 |
更新阶段 | 组件内 beforeRouteUpdate (已复用的组件) |
在当前组件的对应路由被复用时调用,例如,当路由参数发生变化时。 |
确认阶段 | 全局 beforeResolve |
在所有组件内守卫和异步路由组件被成功解析后被调用,确保所有必要的准备工作都已完成。 |
渲染阶段 | Vue Router 更新 DOM,渲染新的组件。 | |
进入组件阶段 | 组件内 beforeRouteEnter (即将进入的组件) |
在进入新组件的对应路由时被调用,但不能访问组件实例 this ,因为组件实例尚未创建。可以通过 next 的回调函数访问组件实例。 |
完成阶段 | 全局 afterEach |
在每次路由导航完成后被调用,用于记录路由访问日志、发送分析数据等。 |
4. 源码剖析:拨开云雾见天日
让我们深入 Vue Router 的源码,看看这些导航守卫是如何被调用的。
Vue Router 使用 matcher
对象来匹配路由。匹配成功后,会创建一个 Route
对象,包含路由的信息。
在 createRouteMap
函数中,会遍历路由配置,并为每个路由创建对应的记录 (record)。这些记录包含了路由的 path、component、name 等信息,以及 beforeEnter
守卫。
// 简化后的 createRouteMap 函数
function createRouteMap (routes: Array<RouteConfig>, oldPathList?: Array<string>, oldPathMap?: Dictionary<RouteRecord>, oldNameMap?: Dictionary<RouteRecord>): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
return {
pathList,
pathMap,
nameMap
}
}
function addRouteRecord (pathList: Array<string>, pathMap: Dictionary<RouteRecord>, nameMap: Dictionary<RouteRecord>, route: RouteConfig, parent?: RouteRecord, matchAs?: string) {
const { path, name } = route
const normalizedPath = normalizePath(path, parent)
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter, // 重点:保存 beforeEnter 守卫
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props },
children: route.children,
alias: route.alias || []
}
// ... 省略其他逻辑
}
在 transitionTo
函数中,Vue Router 会执行导航守卫。这个函数负责找到匹配的路由,并调用相应的守卫。
// 简化后的 transitionTo 函数
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
let route;
try {
route = this.router.match(location, this.current);
} catch (e) {
this.errorCbs.forEach(cb => {
cb(e);
});
// Exception should still be thrown
throw e;
}
const prev = this.current;
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.suppressRedirect) {
if (isError(err)) {
warn(false, 'uncaught error during route navigation:');
console.error(err);
}
}
}
);
}
confirmTransition
函数是执行导航守卫的核心。它会创建一个守卫队列,按照一定的顺序将守卫函数添加到队列中,然后依次执行。
// 简化后的 confirmTransition 函数
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current;
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err);
});
} else {
warn(false, 'uncaught error during route navigation:');
console.error(err);
}
}
onAbort && onAbort(err);
};
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically updated
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort(createNavigationCancelledError(route, current));
}
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(this.current.matched),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(route.matched, current.matched),
// in-config enter guards
route.matched.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(route.matched)
);
this.router.resolveHooks.forEach(hook => {
queue.push(hook)
})
queue.push((to, from, next) => {
this.handleRouteEntered(to);
next()
})
runQueue(queue, iterator, () => {
const resolvedQueue = [].concat(
// global after hooks
this.router.afterHooks.map(hook => {
return (to, from) => {
hook && hook(to, from)
}
})
)
runQueue(resolvedQueue, iterator, () => {
onComplete(route)
})
})
}
extractLeaveGuards
、extractUpdateHooks
函数用于提取组件内的守卫。resolveAsyncComponents
函数用于解析异步组件。
runQueue
函数用于依次执行守卫队列中的函数。
const 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)
}
5. 代码示例:实战演练
让我们通过一些代码示例来加深理解。
5.1 beforeEach
:身份验证
const router = new VueRouter({
routes: [
{
path: '/home',
component: Home
},
{
path: '/login',
component: Login
},
{
path: '/profile',
component: Profile,
meta: { requiresAuth: true } // 需要登录才能访问
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// 需要登录才能访问
if (!localStorage.getItem('token')) {
// 没有登录,跳转到登录页面
next({
path: '/login',
query: { redirect: to.fullPath } // 登录后重定向到目标页面
})
} else {
// 已经登录,允许访问
next()
}
} else {
// 不需要登录,允许访问
next()
}
})
5.2 beforeResolve
:数据预取
const router = new VueRouter({
routes: [
{
path: '/post/:id',
component: Post,
props: true
}
]
})
router.beforeResolve((to, from, next) => {
// 在路由跳转前,预取文章数据
getPost(to.params.id)
.then(post => {
to.params.post = post // 将文章数据添加到路由参数中
next()
})
.catch(error => {
console.error('Failed to fetch post:', error)
next(false) // 中断导航
})
})
5.3 afterEach
:页面统计
const router = new VueRouter({
routes: [...]
})
router.afterEach((to, from) => {
// 记录页面访问日志
console.log(`Navigated from ${from.fullPath} to ${to.fullPath}`)
// 发送页面统计数据
sendPageView(to.fullPath)
})
6. 总结:路由冒险的收获
通过今天的学习,我们了解了 Vue Router 导航守卫的执行流程和作用。
beforeEach
用于全局的身份验证、权限控制等。beforeResolve
用于在路由跳转前进行一些最终的检查和数据预取。afterEach
用于记录页面访问日志、发送统计数据等。
掌握这些导航守卫,可以更好地控制路由跳转的行为,为用户提供更流畅的体验。
希望今天的讲座对你有所帮助。 谢谢大家! 祝大家编码愉快,bug 远离!