Appearance
第8.2节:Vue Router - 如何实现一个前端路由?(下:导航守卫与切换流程)
在上一节中,我们深入分析了Vue Router的核心架构和路由匹配器的实现原理。本节将重点探讨Vue Router的导航守卫系统、完整的路由切换流程、异步路由处理以及路由元信息的应用。
1. 导航守卫系统
1.1 守卫类型与层次结构
Vue Router提供了三个层次的导航守卫:
全局守卫
typescript
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 在每次导航前执行
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
// 全局解析守卫
router.beforeResolve((to, from, next) => {
// 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用
next()
})
// 全局后置钩子
router.afterEach((to, from, failure) => {
// 导航完成后执行,不接受next参数
if (failure) {
console.error('Navigation failed:', failure)
}
})路由独享守卫
typescript
const routes = [
{
path: '/admin',
component: AdminComponent,
beforeEnter: (to, from, next) => {
// 只在进入该路由时触发
if (hasAdminPermission()) {
next()
} else {
next('/403')
}
}
}
]组件内守卫
typescript
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被确认前调用
// 不能获取组件实例 `this`,因为当守卫执行前,组件实例还没被创建
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 可以访问组件实例 `this`
this.name = to.params.id
next()
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
const answer = window.confirm('Do you really want to leave?')
if (answer) {
next()
} else {
next(false)
}
}
}1.2 守卫实现原理
守卫注册机制
typescript
// router.ts
function createRouter(options: RouterOptions): Router {
const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
const afterGuards = useCallbacks<NavigationHookAfter>()
const router: Router = {
beforeEach: beforeGuards.add,
beforeResolve: beforeResolveGuards.add,
afterEach: afterGuards.add,
// ...
}
return router
}守卫执行转换
typescript
// navigationGuards.ts
export function guardToPromiseFn(
guard: NavigationGuard,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded,
record?: RouteRecordNormalized,
name?: string,
runWithContext: <T>(fn: () => T) => T = fn => fn()
): () => Promise<void> {
return () =>
new Promise((resolve, reject) => {
const next: NavigationGuardNext = (
valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
) => {
if (valid === false) {
// 阻止导航
reject(createRouterError(ErrorTypes.NAVIGATION_ABORTED, { from, to }))
} else if (valid instanceof Error) {
// 抛出错误
reject(valid)
} else if (isRouteLocation(valid)) {
// 重定向
reject(createRouterError(ErrorTypes.NAVIGATION_GUARD_REDIRECT, {
from: to,
to: valid,
}))
} else {
// 继续导航
if (enterCallbackArray && typeof valid === 'function') {
enterCallbackArray.push(valid)
}
resolve()
}
}
// 执行守卫函数
const guardReturn = runWithContext(() =>
guard.call(record && record.instances[name!], to, from, next)
)
let guardCall = Promise.resolve(guardReturn)
// 如果守卫函数参数少于3个,自动调用next
if (guard.length < 3) guardCall = guardCall.then(next)
guardCall.catch(reject)
})
}2. 完整的导航流程
2.1 导航触发与解析
当用户触发路由导航时(如调用router.push()),Vue Router会执行以下流程:
typescript
// router.ts
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
const from = currentRoute.value
// 处理重定向
const shouldRedirect = handleRedirectRecord(targetLocation)
if (shouldRedirect) {
return pushWithRedirect(shouldRedirect, redirectedFrom || targetLocation)
}
// 检查是否为重复导航
if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
const failure = createRouterError(ErrorTypes.NAVIGATION_DUPLICATED, {
to: toLocation,
from
})
return Promise.resolve(failure)
}
// 执行导航
return navigate(toLocation, from)
.catch(error => {
// 处理导航错误
return isNavigationFailure(error) ? error : triggerError(error, toLocation, from)
})
.then(failure => {
if (failure) {
// 处理重定向
if (isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)) {
return pushWithRedirect(failure.to, redirectedFrom || toLocation)
}
} else {
// 完成导航
failure = finalizeNavigation(toLocation, from, true, replace, data)
}
// 触发afterEach钩子
triggerAfterEach(toLocation, from, failure)
return failure
})
}2.2 守卫执行序列
typescript
// router.ts
function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): Promise<any> {
let guards: Lazy<any>[]
// 1. 提取变化的路由记录
const [leavingRecords, updatingRecords, enteringRecords] =
extractChangingRecords(to, from)
// 2. 执行beforeRouteLeave守卫
guards = extractComponentsGuards(
leavingRecords.reverse(),
'beforeRouteLeave',
to,
from
)
// 添加路由记录的leaveGuards
for (const record of leavingRecords) {
record.leaveGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from)
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
.then(() => {
// 3. 执行全局beforeEach守卫
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 4. 执行beforeRouteUpdate守卫
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 5. 执行路由独享beforeEnter守卫
guards = []
for (const record of enteringRecords) {
if (record.beforeEnter) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter) {
guards.push(guardToPromiseFn(beforeEnter, to, from))
}
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 6. 解析异步路由组件
to.matched.forEach(record => (record.enterCallbacks = {}))
// 7. 执行beforeRouteEnter守卫
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from,
runWithContext
)
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// 8. 执行全局beforeResolve守卫
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
}2.3 守卫队列执行
typescript
// router.ts
function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
return guards.reduce(
(promise, guard) => promise.then(() => runWithContext(guard)),
Promise.resolve()
)
}2.4 导航完成
typescript
// router.ts
function finalizeNavigation(
toLocation: RouteLocationNormalizedLoaded,
from: RouteLocationNormalizedLoaded,
isPush: boolean,
replace?: boolean,
data?: HistoryState
): NavigationFailure | void {
// 检查导航是否被取消
const error = checkCanceledNavigation(toLocation, from)
if (error) return error
const isFirstNavigation = from === START_LOCATION_NORMALIZED
const state = !isBrowser ? {} : history.state
// 更新浏览器URL
if (isPush) {
if (replace || isFirstNavigation) {
routerHistory.replace(toLocation.fullPath, data)
} else {
routerHistory.push(toLocation.fullPath, data)
}
}
// 更新当前路由
currentRoute.value = toLocation
// 处理滚动行为
handleScroll(toLocation, from, isPush, isFirstNavigation)
// 标记路由器为就绪状态
markAsReady()
}3. 异步路由与懒加载
3.1 异步组件定义
typescript
// 路由配置中的异步组件
const routes = [
{
path: '/about',
// 懒加载组件
component: () => import('./views/About.vue')
},
{
path: '/user',
components: {
default: () => import('./views/User.vue'),
sidebar: () => import('./components/UserSidebar.vue')
}
}
]3.2 异步组件解析
typescript
// navigationGuards.ts
export function extractComponentsGuards(
matched: RouteRecordNormalized[],
guardType: GuardType,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded,
runWithContext: <T>(fn: () => T) => T = fn => fn()
) {
const guards: Array<() => Promise<void>> = []
for (const record of matched) {
for (const name in record.components) {
let rawComponent = record.components[name]
// 跳过未挂载的组件(用于update和leave守卫)
if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue
if (isRouteComponent(rawComponent)) {
// 同步组件
const options = rawComponent.__vccOpts || rawComponent
const guard = options[guardType]
guard && guards.push(
guardToPromiseFn(guard, to, from, record, name, runWithContext)
)
} else {
// 异步组件
let componentPromise = (rawComponent as Lazy<RouteComponent>)()
guards.push(() =>
componentPromise.then(resolved => {
if (!resolved) {
throw new Error(
`Couldn't resolve component "${name}" at "${record.path}"`
)
}
const resolvedComponent = isESModule(resolved)
? resolved.default
: resolved
// 缓存解析后的组件
record.mods[name] = resolved
record.components![name] = resolvedComponent
// 执行组件守卫
const options = resolvedComponent.__vccOpts || resolvedComponent
const guard = options[guardType]
return guard &&
guardToPromiseFn(guard, to, from, record, name, runWithContext)()
})
)
}
}
}
return guards
}3.3 路由预加载
typescript
// navigationGuards.ts
export function loadRouteLocation(
route: RouteLocation | RouteLocationNormalized
): Promise<RouteLocationNormalizedLoaded> {
return route.matched.every(record => record.redirect)
? Promise.reject(new Error('Cannot load a route that redirects.'))
: Promise.all(
route.matched.map(
record =>
record.components &&
Promise.all(
Object.keys(record.components).reduce(
(promises, name) => {
const rawComponent = record.components![name]
if (
typeof rawComponent === 'function' &&
!('displayName' in rawComponent)
) {
promises.push(
(rawComponent as Lazy<RouteComponent>)().then(
resolved => {
if (!resolved) {
return Promise.reject(
new Error(
`Couldn't resolve component "${name}" at "${record.path}"`
)
)
}
const resolvedComponent = isESModule(resolved)
? resolved.default
: resolved
record.mods[name] = resolved
record.components![name] = resolvedComponent
return
}
)
)
}
return promises
},
[] as Array<Promise<RouteComponent | null | undefined>>
)
)
)
).then(() => route as RouteLocationNormalizedLoaded)
}4. 路由元信息(Meta)
4.1 Meta字段定义
typescript
// types/index.ts
export interface RouteMeta extends Record<PropertyKey, unknown> {}
// 扩展RouteMeta接口
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
title?: string
keepAlive?: boolean
}
}4.2 Meta字段应用
typescript
// 路由配置
const routes = [
{
path: '/admin',
component: AdminComponent,
meta: {
requiresAuth: true,
roles: ['admin'],
title: 'Admin Panel'
}
},
{
path: '/profile',
component: ProfileComponent,
meta: {
requiresAuth: true,
keepAlive: true
}
}
]
// 在导航守卫中使用meta
router.beforeEach((to, from, next) => {
// 权限检查
if (to.meta.requiresAuth) {
if (!isAuthenticated()) {
next('/login')
return
}
if (to.meta.roles && !hasRole(to.meta.roles)) {
next('/403')
return
}
}
// 设置页面标题
if (to.meta.title) {
document.title = to.meta.title
}
next()
})4.3 Meta字段合并
typescript
// matcher/index.ts
function mergeMetaFields(matched: MatcherLocation['matched']) {
return matched.reduce(
(meta, record) => assign(meta, record.meta),
{} as MatcherLocation['meta']
)
}
// 在路由解析时合并meta
function resolve(
location: MatcherLocationRaw,
currentLocation: MatcherLocationNormalized
): MatcherLocation {
// ...
return {
name,
path,
params,
matched,
meta: mergeMetaFields(matched), // 合并所有匹配路由的meta
}
}5. next()函数机制
5.1 next函数的作用
next()函数是导航守卫的核心控制机制:
typescript
const next: NavigationGuardNext = (
valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback | Error
) => {
if (valid === false) {
// 阻止当前导航
reject(createRouterError(ErrorTypes.NAVIGATION_ABORTED, { from, to }))
} else if (valid instanceof Error) {
// 抛出错误
reject(valid)
} else if (isRouteLocation(valid)) {
// 重定向到新位置
reject(createRouterError(ErrorTypes.NAVIGATION_GUARD_REDIRECT, {
from: to,
to: valid,
}))
} else {
// 继续导航
if (enterCallbackArray && typeof valid === 'function') {
// beforeRouteEnter的回调函数
enterCallbackArray.push(valid)
}
resolve()
}
}5.2 next函数的不同用法
typescript
// 1. 继续导航
beforeEach((to, from, next) => {
next() // 继续
})
// 2. 阻止导航
beforeEach((to, from, next) => {
next(false) // 阻止导航
})
// 3. 重定向
beforeEach((to, from, next) => {
next('/login') // 重定向到登录页
next({ name: 'Login', query: { redirect: to.fullPath } })
})
// 4. 抛出错误
beforeEach((to, from, next) => {
next(new Error('Something went wrong'))
})
// 5. beforeRouteEnter中的回调
beforeRouteEnter(to, from, next) {
next(vm => {
// 通过vm访问组件实例
vm.setData()
})
}6. 错误处理与调试
6.1 导航错误类型
typescript
// errors.ts
export const enum ErrorTypes {
MATCHER_NOT_FOUND = 1,
NAVIGATION_GUARD_REDIRECT,
NAVIGATION_ABORTED,
NAVIGATION_CANCELLED,
NAVIGATION_DUPLICATED,
}
export interface NavigationFailure {
type: ErrorTypes
from: RouteLocationNormalized
to: RouteLocationNormalized
}6.2 错误处理机制
typescript
// 全局错误处理
router.onError((error, to, from) => {
console.error('Navigation error:', error)
if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
// 导航被阻止
} else if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
// 导航重定向
}
})
// 检查导航失败
router.push('/some-path').catch(failure => {
if (isNavigationFailure(failure, ErrorTypes.NAVIGATION_ABORTED)) {
// 处理导航被阻止的情况
}
})6.3 调试工具
typescript
// 开发环境下的调试信息
if (__DEV__) {
// 检查守卫函数的参数数量
if (guard.length > 2) {
const message = `The "next" callback was never called inside of ${
guard.name ? '"' + guard.name + '"' : ''
}:\n${guard.toString()}\n`
if (typeof guardReturn === 'object' && 'then' in guardReturn) {
guardCall = guardCall.then(resolvedValue => {
if (!next._called) {
warn(message)
return Promise.reject(new Error('Invalid navigation guard'))
}
return resolvedValue
})
}
}
}7. 性能优化策略
7.1 路由懒加载
typescript
// 按需加载路由组件
const routes = [
{
path: '/heavy-component',
component: () => import(/* webpackChunkName: "heavy" */ './HeavyComponent.vue')
}
]
// 预加载关键路由
router.beforeEach((to, from, next) => {
if (to.path === '/important') {
// 预加载相关组件
import('./RelatedComponent.vue')
}
next()
})7.2 守卫优化
typescript
// 避免在守卫中执行重复的异步操作
let userPromise: Promise<User> | null = null
router.beforeEach(async (to, from, next) => {
if (to.meta.requiresAuth) {
// 缓存用户信息请求
if (!userPromise) {
userPromise = fetchUser()
}
try {
const user = await userPromise
if (user) {
next()
} else {
next('/login')
}
} catch (error) {
next('/login')
}
} else {
next()
}
})8. 最佳实践
8.1 守卫设计原则
- 单一职责:每个守卫只处理一种逻辑
- 避免副作用:不要在守卫中修改全局状态
- 错误处理:始终处理可能的异常情况
- 性能考虑:避免在守卫中执行耗时操作
8.2 常见模式
typescript
// 权限控制模式
function createAuthGuard(requiredRoles: string[]) {
return (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
const user = getCurrentUser()
if (!user) {
next('/login')
return
}
if (requiredRoles.some(role => user.roles.includes(role))) {
next()
} else {
next('/403')
}
}
}
// 数据预加载模式
function createDataLoader(loader: (route: RouteLocationNormalized) => Promise<any>) {
return async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
try {
const data = await loader(to)
to.meta.data = data
next()
} catch (error) {
next(error)
}
}
}总结
Vue Router的导航守卫系统提供了强大而灵活的路由控制机制。通过理解守卫的执行顺序、next函数的工作原理以及异步组件的处理方式,我们可以构建出健壮的前端路由系统。
关键要点:
- 守卫层次:全局 → 路由独享 → 组件内,形成完整的控制链
- 执行顺序:严格按照离开 → 更新 → 进入的顺序执行
- 异步处理:支持异步组件和异步守卫,保证组件解析完成
- 错误处理:完善的错误类型和处理机制
- 性能优化:通过懒加载和缓存提升应用性能
在下一节中,我们将探讨如何基于这些原理实现一个简化版的前端路由系统。
