Skip to content

第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 守卫设计原则

  1. 单一职责:每个守卫只处理一种逻辑
  2. 避免副作用:不要在守卫中修改全局状态
  3. 错误处理:始终处理可能的异常情况
  4. 性能考虑:避免在守卫中执行耗时操作

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函数的工作原理以及异步组件的处理方式,我们可以构建出健壮的前端路由系统。

关键要点:

  1. 守卫层次:全局 → 路由独享 → 组件内,形成完整的控制链
  2. 执行顺序:严格按照离开 → 更新 → 进入的顺序执行
  3. 异步处理:支持异步组件和异步守卫,保证组件解析完成
  4. 错误处理:完善的错误类型和处理机制
  5. 性能优化:通过懒加载和缓存提升应用性能

在下一节中,我们将探讨如何基于这些原理实现一个简化版的前端路由系统。


微信公众号二维码