前言
在 Vue 3 项目开发中,我们经常会遇到一些"过度设计"的代码——它们功能完善、设计精巧,但维护成本高、学习曲线陡峭。本文将分享一次真实的重构经历:如何将一个 400 行的自定义规则引擎改造为不到 150 行的标准 Vue 3 架构,同时保持功能完全一致。
问题背景
原有架构的痛点
我们的项目中有一个用户认证服务 VerificationCenter,它采用了自定义规则引擎模式:
// 原有架构示意
VerificationCenter.init({ router, store, i18n })
VerificationCenter.register(userTypeAuthRule)
VerificationCenter.register(enterpriseCertRule)
VerificationCenter.run('login')
VerificationCenter.run('routeChange')
这个设计存在以下问题:
- 过度抽象:引入了规则注册、拓扑排序、依赖管理等复杂概念
- 学习成本高:新人需要理解整个规则引擎的运作机制
- 代码量大:核心代码超过 400 行,维护困难
- 非标准模式:不符合 Vue 3 社区最佳实践
- 模块级状态:使用模块级可变状态,容易产生副作用
重构目标
- ✅ 使用 Vue 3 标准模式(Router Guard + Pinia Store + Composable)
- ✅ 代码量减少到 150 行以内
- ✅ 保持功能完全一致
- ✅ 提升代码可维护性
- ✅ 降低学习成本
重构方案
架构对比
重构前:
┌─────────────────────────────────────┐
│ VerificationCenter │
│ ┌───────────────────────────────┐ │
│ │ 规则注册机制 │ │
│ │ 拓扑排序 │ │
│ │ 依赖管理 │ │
│ │ 模块级状态 │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
重构后:
┌──────────────────────────────────────────┐
│ Vue Router Guard │
│ (afterEach - 路由切换时自动触发) │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ useAuthPrompt() │
│ (Composable - 封装弹窗逻辑) │
└──────────────────────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ Pinia User Store │
│ (响应式状态管理) │
└──────────────────────────────────────────┘
核心实现
1. Pinia Store 管理认证状态
首先,在现有的 User Store 中扩展认证相关的状态和计算属性:
// src/store/core/user.ts
// 认证提示类型
export type AuthPromptType =
| 'personal_inactive' // 个人用户未激活
| 'enterprise_unverified' // 企业用户未认证
| 'enterprise_pending' // 企业用户审核中
| null
// 状态
const promptShownInSession = ref(false)
// 计算属性:是否需要认证提示
const needsAuthPrompt = computed(() => {
if (promptShownInSession.value) return false
const type = userInfo.value.userType
return type === 45 || type === 40
})
// 计算属性:认证提示类型
const authPromptType = computed((): AuthPromptType => {
const type = userInfo.value.userType
if (type === 45) return 'personal_inactive'
if (type === 40) {
return userInfo.value.enterpriseCheckFlag
? 'enterprise_unverified'
: 'enterprise_pending'
}
return null
})
// 操作:标记提示已显示
function markPromptShown() {
promptShownInSession.value = true
sessionStorage.setItem('auth_prompt_shown', '1')
}
// 操作:恢复提示状态
function restorePromptState() {
promptShownInSession.value =
sessionStorage.getItem('auth_prompt_shown') === '1'
}
设计亮点:
- 使用
computed实现响应式状态计算 - 利用
sessionStorage持久化状态,避免重复提示 - 类型安全的 TypeScript 定义
2. Composable 封装弹窗逻辑
创建一个专门的 Composable 来处理认证提示弹窗:
// src/composables/auth/useAuthPrompt.ts
import { createApp, h } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '~/store/core/user'
import CmcConfirm from '~/components/CmcConfirm'
// 模块级变量:当前弹窗实例
let currentDialog: {
app: ReturnType<typeof createApp>
container: HTMLElement
} | null = null
export function useAuthPrompt() {
const router = useRouter()
const userStore = useUserStore()
const { t } = useI18n()
// 关闭弹窗
function closePrompt() {
if (currentDialog) {
currentDialog.app.unmount()
currentDialog.container.remove()
currentDialog = null
}
}
// 显示弹窗
function showPrompt() {
closePrompt()
const promptType = userStore.authPromptType
if (!promptType) return
const messages = getPromptMessages(promptType, t)
if (!messages.content) return
// 审核中状态:只显示消息提示
if (promptType === 'enterprise_pending') {
import('element-plus').then(({ ElMessage }) => {
ElMessage.info(messages.content)
})
userStore.markPromptShown()
return
}
// 创建弹窗
const container = document.createElement('div')
document.body.appendChild(container)
const app = createApp({
render: () => h(CmcConfirm, {
type: 'warning',
title: messages.title,
content: messages.content,
modelValue: true,
buttons: [
{
text: messages.confirmText,
type: 'primary',
onClick: () => {
closePrompt()
userStore.markPromptShown()
}
},
{
text: messages.cancelText,
type: 'default',
onClick: () => {
closePrompt()
userStore.markPromptShown()
}
},
],
showDefaultButtons: false,
onClose: () => {
closePrompt()
userStore.markPromptShown()
},
})
})
app.mount(container)
currentDialog = { app, container }
}
// 检查并显示(路由守卫调用)
function checkAndShow(route: any) {
// blank 布局跳过
if (route?.meta?.layout === 'blank') {
closePrompt()
return
}
// 需要提示则显示
if (userStore.needsAuthPrompt) {
showPrompt()
}
}
// 监听路由变化
router.afterEach((to) => {
if (to.meta?.layout === 'blank') {
closePrompt()
}
})
return {
checkAndShow,
showPrompt,
closePrompt,
}
}
// 获取提示消息
function getPromptMessages(
type: AuthPromptType,
t: (key: string) => string
) {
const title = t('common.confirmTitle')
const cancelText = t('common.cancel')
switch (type) {
case 'personal_inactive':
return {
title,
content: t('system.sys.lock.notRequiredLockTip'),
confirmText: t('common.ok'),
cancelText,
}
case 'enterprise_unverified':
return {
title,
content: t('system.sys.lock.enterpriseNotVerifyLockTip'),
confirmText: t('system.sys.lock.goVerify') || '去认证',
cancelText,
}
case 'enterprise_pending':
return {
title,
content: t('system.sys.lock.enterpriseVerifyLockTip'),
confirmText: '',
cancelText: '',
}
default:
return {
title: '',
content: '',
confirmText: '',
cancelText: ''
}
}
}
设计亮点:
- 使用
createApp+h函数动态创建弹窗 - 模块级变量管理弹窗实例,确保同时只有一个弹窗
- 自动监听路由变化,切换到登录页时关闭弹窗
- 国际化支持
3. 路由守卫集成
在路由配置中使用 Composable:
// src/router/index.ts
router.afterEach((to, from) => {
// ... 其他逻辑 ...
// 检查认证提示
import('~/composables/auth/useAuthPrompt').then(({ useAuthPrompt }) => {
const { checkAndShow } = useAuthPrompt()
checkAndShow(to)
})
})
设计亮点:
- 使用
afterEach守卫,不阻塞路由导航 - 动态导入 Composable,优化首屏加载
- 简洁明了,一目了然
重构成果
代码量对比
| 指标 | 重构前 | 重构后 | 改善 |
|---|---|---|---|
| 核心代码行数 | ~400 行 | <150 行 | 减少 62.5% |
| 文件数量 | 3 个 | 2 个 | 减少 1 个 |
| 概念复杂度 | 高(规则引擎) | 低(标准模式) | 显著降低 |
架构对比
| 方面 | 重构前 | 重构后 |
|---|---|---|
| 架构模式 | 自定义规则引擎 | Vue 3 标准模式 |
| 状态管理 | 模块级变量 + sessionStorage | Pinia Store(响应式) |
| 触发机制 | 手动调用 run() | 路由守卫自动触发 |
| 学习成本 | 需要理解规则引擎 | 标准 Vue 3 知识即可 |
| 可维护性 | 低 | 高 |
| 可测试性 | 中 | 高 |
功能完整性
✅ 个人用户未激活提示
✅ 企业用户未认证提示
✅ 企业用户审核中提示
✅ 登录页自动关闭弹窗
✅ 会话内不重复提示
✅ 国际化支持
重构经验总结
1. 识别过度设计的信号
- 代码行数远超实际需求
- 引入了不必要的抽象层
- 新人理解困难
- 不符合社区最佳实践
2. 重构的黄金法则
保持功能一致性:重构不是重写,要确保用户体验不变
渐进式改造:
- 先实现新架构
- 并行运行新旧代码
- 验证功能一致性
- 删除旧代码
充分测试:
- 单元测试覆盖核心逻辑
- 集成测试验证完整流程
- 手动测试确保用户体验
3. Vue 3 最佳实践
状态管理:
- 使用 Pinia Store 管理全局状态
- 利用
computed实现响应式计算 - 避免模块级可变状态
逻辑复用:
- 使用 Composable 封装可复用逻辑
- 遵循单一职责原则
- 保持函数纯净
路由集成:
- 使用路由守卫处理导航逻辑
beforeEach用于权限检查(阻塞)afterEach用于副作用操作(非阻塞)
4. 代码质量提升技巧
类型安全:
// ✅ 好的做法:明确的类型定义
export type AuthPromptType =
| 'personal_inactive'
| 'enterprise_unverified'
| 'enterprise_pending'
| null
// ❌ 避免:使用 any 或 string
type AuthPromptType = string | null
错误处理:
// ✅ 好的做法:静默失败,不阻塞用户
function checkAndShow(route: any) {
try {
// ... 逻辑 ...
} catch (error) {
console.warn('[AuthPrompt] 检查失败:', error)
// 不抛出错误,不阻塞路由
}
}
资源清理:
// ✅ 好的做法:及时清理资源
function closePrompt() {
if (currentDialog) {
currentDialog.app.unmount() // 卸载 Vue 实例
currentDialog.container.remove() // 移除 DOM 节点
currentDialog = null // 释放引用
}
}
可复用的重构模式
模式 1:规则引擎 → 路由守卫
适用场景:需要在路由切换时执行检查或操作
重构步骤:
- 识别触发时机(login、routeChange 等)
- 将逻辑迁移到对应的路由守卫
- 使用 Composable 封装复杂逻辑
模式 2:模块级状态 → Pinia Store
适用场景:需要管理全局状态
重构步骤:
- 创建或扩展 Pinia Store
- 使用
ref定义响应式状态 - 使用
computed定义计算属性 - 使用
action定义状态变更方法
模式 3:命令式调用 → 声明式响应
适用场景:需要根据状态自动触发操作
重构步骤:
- 将状态提升到 Store
- 使用
watch或watchEffect监听状态变化 - 在路由守卫中检查状态并执行操作
总结
这次重构让我们深刻体会到:好的架构不是最复杂的,而是最合适的。通过拥抱 Vue 3 的标准模式,我们不仅减少了代码量,更重要的是提升了代码的可维护性和可读性。
重构的核心价值在于:
- 降低认知负担
- 提升开发效率
- 减少维护成本
- 符合社区规范
希望这次重构经验能为你的项目提供参考。记住:简单,往往更强大。
参考资源
如果这篇文章对你有帮助,欢迎点赞、收藏、分享!有任何问题欢迎在评论区讨论。