news 2026/4/16 16:44:23

Vue 3 路由守卫中安全使用 Composition API 的最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 路由守卫中安全使用 Composition API 的最佳实践

背景与问题

业务场景

在企业级 SaaS 应用中,我们需要在用户登录后进行多种认证检查:

  1. 用户类型认证:个人用户需激活、企业用户需完成认证
  2. 密码过期检查:强制用户定期更新密码
  3. 权限验证:不同用户类型访问不同功能模块

最初的实现采用了"认证服务中心"(VerificationCenter)的设计模式,通过规则引擎统一管理所有认证逻辑。

遇到的问题

// ❌ 错误示例:在路由守卫中直接调用 Composition API router.afterEach((to, from) => { const { checkAndShowAuthDialog } = useUserTypeAuth() checkAndShowAuthDialog() })

报错信息

SyntaxError: Must be called at the top of a `setup` function at useI18n (vue-i18n.js:314:17) at useConfirm (useConfirm.ts:165:17) at useUserTypeAuth (useUserTypeAuth.ts:72:23)

问题根源

Vue 3 的 Composition API(如useI18nuseRouteruseRoute必须在 Vue 组件的setup函数顶层调用,而路由守卫运行在 Vue 组件上下文之外,直接调用会触发运行时错误。


核心问题分析

1. Composition API 的上下文限制

// Vue 3 内部实现(简化版) let currentInstance: ComponentInternalInstance | null = null export function getCurrentInstance(): ComponentInternalInstance | null { return currentInstance } export function useRouter(): Router { const instance = getCurrentInstance() if (!instance) { throw new Error('Must be called at the top of a setup function') } return instance.appContext.config.globalProperties.$router }

关键点

  • Composition API 依赖currentInstance获取 Vue 实例上下文
  • 路由守卫执行时currentInstancenull
  • 直接调用会抛出异常

2. 架构过度设计的反思

原有的 VerificationCenter 架构:

// 规则引擎模式 interface VerificationRule { id: string when: ('login' | 'appReady' | 'routeChange')[] shouldRun: (ctx: VerificationContext) => Promise<boolean> run: (ctx: VerificationContext) => Promise<void> } class VerificationCenter { private rules: Map<string, VerificationRule> = new Map() register(rule: VerificationRule) { this.rules.set(rule.id, rule) } async run(trigger: string) { for (const rule of this.rules.values()) { if (rule.when.includes(trigger)) { if (await rule.shouldRun(ctx)) { await rule.run(ctx) } } } } }

问题分析

  • 优点:高度抽象、易扩展、规则解耦
  • 缺点
    • 只有 3 个规则,不需要如此复杂的架构
    • 增加认知负担,新人难以理解
    • 调用链路长,调试困难
    • 性能开销(函数调用、对象创建)
    • 违反 YAGNI 原则(You Aren't Gonna Need It)

解决方案设计

设计原则

  1. 简单性优于灵活性:当前需求简单,不需要过度设计
  2. SOLID 原则:单一职责、开闭原则
  3. 上下文隔离:路由守卫专用函数不依赖 Vue 上下文

架构对比

方案一:直接调用(✅ 采用)
// 路由守卫中直接调用 router.afterEach((to) => { checkAuthInRouterGuard() checkPasswordExpiredInRouterGuard(router) })

优点

  • 代码简洁直观
  • 调用链路清晰
  • 易于调试和维护
  • 性能最优
方案二:保留规则引擎(❌ 放弃)

缺点

  • 过度设计,增加复杂度
  • 不符合当前业务规模
  • 维护成本高

技术方案

核心思路:为路由守卫创建独立的、不依赖 Composition API 的函数。

// 设计模式:Adapter Pattern(适配器模式) // 将依赖 Composition API 的逻辑适配为独立函数 // 1. 组件内使用(依赖 Composition API) export function useUserTypeAuth() { const router = useRouter() // ✅ 在 setup 中调用 const { t } = useI18n() // ✅ 在 setup 中调用 // ... } // 2. 路由守卫使用(不依赖 Composition API) export function checkAuthInRouterGuard(): void { // ✅ 直接从 store 获取数据,不依赖 Vue 上下文 const userType = getUserTypeFromStore() const user = getUserFromStore() // ✅ 使用安全的 i18n 包装器 const t = getSafeI18n() // ✅ 动态导入 router 实例 const handleAction = async () => { const { default: router } = await import('~/router') await router.push('/profile') } }

代码实现

1. 安全的 i18n 包装器

// src/composables/ui/useConfirm.ts /** * 安全获取 i18n 翻译函数 * @description 尝试调用 useI18n(),失败则返回 fallback 翻译 */ function getSafeI18n(): (key: string) => string { try { const { t } = useI18n() return t } catch { // 路由守卫等非 Vue 组件上下文中使用 fallback return (key: string) => { const fallbacks: Record<string, string> = { 'common.confirmTitle': '提示', 'common.ok': '确定', 'common.cancel': '取消', 'common.closeWindow': '关闭窗口', 'ui.confirm.cancelTask': '取消任务', 'ui.confirm.continueOperation': '继续操作', } return fallbacks[key] || key } } } export function useConfirm() { const t = getSafeI18n() // ✅ 安全调用 const confirm = (message: string, options?: ConfirmOptions) => { return ElMessageBox.confirm(message, { title: options?.title || t('common.confirmTitle'), confirmButtonText: options?.okBtnText || t('common.ok'), cancelButtonText: t('common.cancel'), type: options?.type || 'info', // ... }) } return { confirm, alert } }

设计亮点

  • 优雅降级:有 Vue 上下文时使用useI18n(),否则使用 fallback
  • 零侵入:不影响现有组件的使用方式
  • 类型安全:保持完整的 TypeScript 类型推导

2. 用户认证检查(路由守卫专用)

// src/composables/auth/useUserTypeAuth.ts /** * 路由守卫中检查用户认证状态 * @description 不依赖 Composition API,可在路由守卫中安全调用 */ export function checkAuthInRouterGuard(): void { // 1. 从 store 获取数据(不依赖 Vue 上下文) const userType = getUserTypeFromStore() if (!needsAuthPrompt(userType)) { return } const user = getUserFromStore() if (!user) { return } // 2. 使用安全的 confirm(内部使用 getSafeI18n) const { confirm } = useConfirm() const t = getSafeI18n() // 3. 获取提示配置 const config = getAuthPromptMessage(userType, user, t) if (!config.content) { return } // 4. 显示确认对话框 void (async () => { try { if (config.showConfirmBtn) { await confirm(config.content, { title: config.title, type: 'warning', buttons: [ { text: config.confirmText, type: 'primary', customClass: 'customer-button-default customer-primary-button customer-button', onClick: async () => { // ✅ 动态导入 router,避免循环依赖 const { default: router } = await import('~/router') await executeAuthActionForService(userType, user, router) }, }, { text: config.cancelText, type: 'default', customClass: 'trans-bg-btn', }, ], }) } else { // 企业认证审核中:仅显示提示,使用文字按钮 await confirm(config.content, { title: config.title, type: 'warning', buttons: [ { text: config.confirmText, type: 'primary', link: true, // ✅ 文字按钮样式 }, ], }) } } catch { // 用户取消操作 } })() } // ==================== 辅助函数 ==================== /** * 从 Store 获取用户类型 */ function getUserTypeFromStore(): number { const userStore = useUserStoreWithOut() return userStore.userInfo?.userType ?? 0 } /** * 从 Store 获取用户信息 */ function getUserFromStore(): any { const userStore = useUserStoreWithOut() return userStore.userInfo } /** * 判断是否需要显示认证提示 */ function needsAuthPrompt(userType: number): boolean { const NEEDS_AUTH_PROMPT_USER_TYPES = [1, 2, 3, 4] return NEEDS_AUTH_PROMPT_USER_TYPES.includes(userType) } /** * 获取认证提示消息配置 */ function getAuthPromptMessage( userType: number, user: any, t: (key: string) => string, ): AuthPromptConfig { // 个人用户:未激活 if (userType === 1 && user.userStatus === 0) { return { title: t('register.personalActivation'), content: t('register.personalActivationTip'), confirmText: t('register.goActivate'), cancelText: t('common.cancel'), showConfirmBtn: true, } } // 企业用户:认证审核中 if ([2, 3, 4].includes(userType) && user.verificationStatus === 1) { return { title: t('register.enterpriseCertification'), content: t('register.enterpriseCertificationPendingTip'), confirmText: t('common.ok'), cancelText: '', showConfirmBtn: false, // ✅ 仅显示提示,不需要确认按钮 } } // 企业用户:认证被拒绝 if ([2, 3, 4].includes(userType) && user.verificationStatus === 3) { return { title: t('register.enterpriseCertification'), content: t('register.enterpriseCertificationRejectedTip'), confirmText: t('register.goResubmit'), cancelText: t('common.cancel'), showConfirmBtn: true, } } return { title: '', content: '', confirmText: '', cancelText: '', showConfirmBtn: false } }

设计亮点

  • 职责分离:数据获取、逻辑判断、UI 展示分离
  • 可测试性:纯函数设计,易于单元测试
  • 动态导入:避免循环依赖,按需加载
  • 错误处理:优雅处理用户取消操作

3. 密码过期检查(路由守卫专用)

// src/composables/auth/usePasswordExpired.ts /** * 路由守卫中检查密码过期并显示重置弹窗 * @description 不依赖 Composition API,可在路由守卫中安全调用 * @param routerInstance - 路由实例 */ export function checkPasswordExpiredInRouterGuard(routerInstance: Router): void { const route = routerInstance.currentRoute.value // 1. 跳过 blank 布局页面(登录、注册等) const isBlank = route?.meta?.layout === 'blank' if (isBlank) return // 2. 只在内部页面检查 const category = route?.meta?.category if (category !== 'internal') return // 3. 检查 sessionStorage 标记 const forceTokenReset = sessionStorage.getItem('vc_force_reset_pwd') === '1' const forceSelfReset = sessionStorage.getItem('vc_force_reset_pwd_self') === '1' if (forceTokenReset || forceSelfReset) { showResetPasswordDialogStandalone(routerInstance) } } /** * 显示密码重置弹窗(独立函数,不依赖 Composition API) * @param routerInstance - 路由实例 */ function showResetPasswordDialogStandalone(routerInstance: Router): void { const container = document.createElement('div') document.body.appendChild(container) // ✅ 使用 createApp 动态挂载组件 const app = createApp({ render() { const useTokenMode = sessionStorage.getItem('vc_force_reset_pwd') === '1' return h(ResetPassWord, { size: 'large', force: true, useToken: useTokenMode, onSuccess: () => { // 清理标记 try { sessionStorage.removeItem('vc_force_reset_pwd') sessionStorage.removeItem('vc_force_reset_pwd_self') sessionStorage.removeItem('vc_pwd_reset_token') sessionStorage.removeItem('vc_origin_password') } catch {} // 清理登录状态 common.setWindowKeyValue('pwd_reset_token', undefined) common.removeLoginAuthToken() window.sessionStorage.clear() // 跳转到登录页 routerInstance.replace(RouteConfig.Login.path) // 卸载组件 app.unmount() if (container.parentNode) container.parentNode.removeChild(container) }, onClose: () => { app.unmount() if (container.parentNode) container.parentNode.removeChild(container) }, }) }, }) // ✅ 注入全局 i18n(从 window 获取,避免依赖 useI18n) try { if ((window as any).i18n) { app.use((window as any).i18n) } } catch {} app.mount(container) // ✅ 监听路由变化,自动关闭弹窗 try { const unwatch = routerInstance.afterEach((to: any) => { const isBlank = to?.meta?.layout === 'blank' if (isBlank) { try { sessionStorage.removeItem('vc_force_reset_pwd') sessionStorage.removeItem('vc_force_reset_pwd_self') } catch {} app.unmount() if (container.parentNode) container.parentNode.removeChild(container) unwatch() } }) } catch {} }

设计亮点

  • 动态挂载:使用createApp+h()渲染函数动态创建组件实例
  • 生命周期管理:自动清理 DOM 和事件监听器
  • 全局 i18n 注入:从window获取全局 i18n 实例,避免依赖useI18n()
  • 路由监听:自动响应路由变化,关闭弹窗

4. 路由守卫集成

// src/router/index.ts import { createRouter, createWebHashHistory } from 'vue-router' import { checkPasswordExpiredInRouterGuard } from '~/composables/auth/usePasswordExpired' import { checkAuthInRouterGuard } from '~/composables/auth/useUserTypeAuth' const router = createRouter({ history: createWebHashHistory(), routes, }) // ==================== 路由后置守卫 ==================== router.afterEach((to, from) => { try { // Keep-Alive 缓存管理 const keepAliveStore = useKeepAliveStoreWithOut() if (to.meta?.keepAlive && to.name) { keepAliveStore.addCachedView(to.name as string) } if (from.meta?.noCache && from.name) { keepAliveStore.deleteCachedView(from.name as string) } // 重置滚动位置 window.scrollTo({ top: 0, left: 0, behavior: 'auto' }) // 停止进度条 setTimeout(() => { nprogressManager.done() CmcLoadingService.closeAll() }, 300) // ==================== 认证检查 ==================== // 跳过 blank 布局页面(登录、注册等) if (to.meta?.layout !== 'blank') { // ✅ 用户认证检查(不依赖 Composition API) checkAuthInRouterGuard() // ✅ 密码过期检查(不依赖 Composition API) checkPasswordExpiredInRouterGuard(router) } } catch (error) { console.error('路由后置守卫执行失败:', error) } }) export default router

设计亮点

  • 清晰的职责划分:缓存管理、滚动控制、认证检查分离
  • 错误边界:统一的 try-catch 错误处理
  • 条件执行:根据路由元信息决定是否执行检查

架构优化思考

1. YAGNI 原则的实践

You Aren't Gonna Need It(你不会需要它)

// ❌ 过度设计:为未来可能的需求预留扩展 class VerificationCenter { private rules: Map<string, VerificationRule> = new Map() private middleware: Middleware[] = [] private eventBus: EventEmitter = new EventEmitter() async run(trigger: string, ctx: VerificationContext) { // 复杂的规则引擎逻辑 // 中间件机制 // 事件发布订阅 } } // ✅ 简单设计:满足当前需求即可 export function checkAuthInRouterGuard(): void { // 直接实现业务逻辑 }

反思

  • 当前只有 3 个认证规则,不需要规则引擎
  • 未来如果真的需要扩展(如增加到 10+ 规则),再重构也不迟
  • 过早优化是万恶之源

2. 简单性 vs 灵活性

维度规则引擎(复杂)直接调用(简单)
代码行数~500 行~200 行
认知负担高(需理解规则引擎)低(直接阅读业务逻辑)
调试难度困难(调用链长)简单(调用链短)
扩展性高(添加规则)中(直接添加函数)
性能中(函数调用开销)高(直接调用)
适用场景10+ 规则3-5 规则

结论:在当前业务规模下,简单性优于灵活性。

3. 上下文隔离的设计模式

// 设计模式:Adapter Pattern(适配器模式) // 1. 组件内使用(依赖 Vue 上下文) export function useUserTypeAuth() { const router = useRouter() // 依赖 Vue 上下文 const { t } = useI18n() // 依赖 Vue 上下文 return { checkAndShowAuthDialog: () => { // 组件内逻辑 } } } // 2. 路由守卫使用(不依赖 Vue 上下文) export function checkAuthInRouterGuard(): void { // 适配器:将依赖 Vue 上下文的逻辑转换为独立函数 const userType = getUserTypeFromStore() // 直接访问 store const t = getSafeI18n() // 安全的 i18n 包装器 // 业务逻辑 }

设计原则

  • 单一职责:每个函数只做一件事
  • 依赖倒置:依赖抽象(store、全局对象)而非具体实现(Vue 实例)
  • 开闭原则:对扩展开放(可添加新的检查函数),对修改封闭(不影响现有逻辑)

4. 错误处理策略

// ✅ 优雅降级 function getSafeI18n(): (key: string) => string { try { const { t } = useI18n() return t } catch { // 降级到 fallback 翻译 return (key: string) => fallbacks[key] || key } } // ✅ 静默失败(用户取消操作) void (async () => { try { await confirm(config.content, options) } catch { // 用户取消,不需要处理 } })() // ✅ 全局错误边界 router.afterEach((to, from) => { try { checkAuthInRouterGuard() checkPasswordExpiredInRouterGuard(router) } catch (error) { console.error('路由后置守卫执行失败:', error) // 不阻断路由导航 } })

原则

  • 优雅降级:功能不可用时提供 fallback
  • 静默失败:用户主动取消的操作不需要错误提示
  • 全局边界:关键路径添加 try-catch,防止整个应用崩溃

最佳实践总结

✅ Do's(推荐做法)

  1. 为路由守卫创建独立函数

// ✅ 独立函数,不依赖 Composition API export function checkAuthInRouterGuard(): void { const userType = getUserTypeFromStore() // ... }

使用安全的 i18n 包装器

// ✅ 优雅降级 function getSafeI18n(): (key: string) => string { try { const { t } = useI18n() return t } catch { return (key: string) => fallbacks[key] || key } }

动态导入避免循环依赖

// ✅ 按需加载 const handleAction = async () => { const { default: router } = await import('~/router') await router.push('/profile') }

从 Store 获取数据,不依赖 Vue 实例

// ✅ 直接访问 store const userStore = useUserStoreWithOut() const userType = userStore.userInfo?.userType

使用 createApp 动态挂载组件

// ✅ 独立的 Vue 应用实例 const app = createApp({ render() { return h(ResetPassWord, { /* props */ }) } }) app.mount(container)

❌ Don'ts(避免做法)

  1. 不要在路由守卫中直接调用 Composition API

// ❌ 会报错 router.afterEach(() => { const router = useRouter() // 错误! const { t } = useI18n() // 错误! })

不要过度设计

// ❌ 3 个规则不需要规则引擎 class VerificationCenter { private rules: Map<string, VerificationRule> = new Map() // 复杂的规则引擎逻辑 }

不要忽略错误处理

// ❌ 没有错误边界 router.afterEach(() => { checkAuth() // 如果出错会导致路由导航失败 }) // ✅ 添加错误边界 router.afterEach(() => { try { checkAuth() } catch (error) { console.error(error) } })

不要忘记清理副作用

// ❌ 没有清理 DOM 和事件监听器 const app = createApp(Component) app.mount(container) // ✅ 清理副作用 const unwatch = router.afterEach(() => { app.unmount() container.remove() unwatch() })

📊 性能优化建议

  1. 避免不必要的检查

// ✅ 提前返回 if (to.meta?.layout === 'blank') return if (to.meta?.category !== 'internal') return

使用 sessionStorage 缓存标记

// ✅ 避免重复检查 const forceReset = sessionStorage.getItem('vc_force_reset_pwd') === '1' if (!forceReset) return

动态导入按需加载

// ✅ 只在需要时加载 const { default: router } = await import('~/router')

🧪 可测试性建议

  1. 纯函数设计

// ✅ 易于测试 function needsAuthPrompt(userType: number): boolean { return [1, 2, 3, 4].includes(userType) } // 测试 expect(needsAuthPrompt(1)).toBe(true) expect(needsAuthPrompt(5)).toBe(false)

依赖注入

// ✅ 可注入 mock router export function checkPasswordExpiredInRouterGuard( routerInstance: Router ): void { // 使用注入的 router 实例 }

职责分离

// ✅ 数据获取、逻辑判断、UI 展示分离 const userType = getUserTypeFromStore() // 数据层 const needsAuth = needsAuthPrompt(userType) // 逻辑层 if (needsAuth) showAuthDialog() // UI 层

总结

核心要点

  1. 理解 Composition API 的上下文限制

    • 必须在 Vue 组件的setup函数顶层调用
    • 路由守卫运行在 Vue 上下文之外
  2. 为路由守卫创建独立函数

    • 不依赖useRouteruseI18n等 Composition API
    • 从 Store 或全局对象获取数据
    • 使用安全的 i18n 包装器
  3. 遵循 YAGNI 原则

    • 不要过度设计
    • 简单性优于灵活性
    • 满足当前需求即可
  4. 优雅的错误处理

    • 优雅降级(fallback)
    • 静默失败(用户取消)
    • 全局错误边界

适用场景

  • ✅ 路由守卫中需要使用 i18n、router 等 Composition API
  • ✅ 需要在非 Vue 组件上下文中执行 Vue 相关逻辑
  • ✅ 需要动态挂载组件(如弹窗、通知)
  • ✅ 需要简化过度设计的架构
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 13:01:50

LoadRunner

可以把它理解为一个非常专业的“压力测试实验室”。就像汽车工厂会在专门的测试场&#xff0c;模拟各种极端路况&#xff08;颠簸、高温、严寒、长时间驾驶&#xff09;来检验车辆性能一样&#xff0c;LoadRunner就是在数字世界里&#xff0c;为网站、应用程序或服务器搭建的这…

作者头像 李华
网站建设 2026/4/10 16:53:09

Supertest

Supertest这个工具&#xff0c;它是我在测试Node.js HTTP API时最信赖的“探测仪器”之一。 &#x1f9e9; Supertest是什么&#xff1f; Supertest是一个轻量级的Node.js测试库&#xff0c;专门用于对HTTP API进行自动化测试。它基于另一个名为SuperAgent的HTTP客户端库构建…

作者头像 李华
网站建设 2026/4/16 13:01:40

完全取代Claude Code?OpenAI反击来了,推出Codex app「限时免费使用」

终于OpenAI的反击还是来了&#xff0c;还是抢在据传Claude sonnet 5发布前一天推出。 多年来我一直是终端/Emacs 的忠实用户&#xff0c;但自从使用 Codex 应用程序后&#xff0c;再回到终端就感觉像是回到了过去。这简直是专为Agent打造的原生开发界面体验。 这是OpenAI总裁Gr…

作者头像 李华
网站建设 2026/4/16 4:18:02

Git Clone

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 基本语法常用场景关键参数克隆后的操作示例&#xff1a;克隆带子模块的仓库注意事项默认行为&#xff1a;克隆所有分支&#xff0c;但仅检出默认分支如何在本地创建…

作者头像 李华