聊聊设计模式在 Vue 3 业务开发中的落地——从一次代码重构说起

最近接手了一个老模块的重构任务,代码里到处都是硬编码的中文字符串,配置散落在各个组件里,状态映射逻辑重复了好几遍。你懂的,就是那种"能跑但看着难受"的代码。

趁着这次重构,我把一些设计模式用在了实际业务里。不是为了炫技,纯粹是解决问题。写完之后觉得挺有收获,分享出来聊聊。

背景:一个典型的后台管理模块

项目是 Vue 3 + TypeScript + Element Plus 的技术栈。这个模块大概长这样:

  • 顶部有一排流程统计卡片
  • 中间是一个多 Tab 切换的数据表格,每个 Tab 对应不同的列配置
  • 底部是图表统计区域
  • 还要支持中英文切换

代码的问题很明显:

typescript 复制代码
// 散落在组件里的配置
const columns = [
  { key: 'orderNo', label: '订单号', minWidth: '140px' },
  { key: 'status', label: '状态', minWidth: '100px' },
  // ...
]

const STATUS_MAP = {
  pending: { text: '待处理', type: 'warning' },
  done: { text: '已完成', type: 'success' },
}

问题来了:

  1. label 写死中文,国际化怎么办?
  2. 配置散落各处,改一个地方要找半天
  3. 状态映射重复,composable 里有一份,组件里又有一份

方案一:工厂函数模式

最直接的问题是国际化。一开始我想用 computed 包一层,但很快发现不对劲------配置对象在模块顶层定义,那时候 i18n 实例还没初始化呢。

解法是把静态配置改成工厂函数

typescript 复制代码
// constants/index.ts
import type { ComposerTranslation } from 'vue-i18n'

// 静态配置:路由、图标这些不需要翻译的
export const ROUTE_CONFIG = {
  detail: '/order/detail',
  edit: '/order/edit',
} as const

// 工厂函数:需要翻译的配置
export function createStatusConfig(t: ComposerTranslation) {
  return {
    pending: { text: t('status.pending'), type: 'warning' },
    processing: { text: t('status.processing'), type: 'info' },
    done: { text: t('status.done'), type: 'success' },
    failed: { text: t('status.failed'), type: 'danger' },
  }
}

export function createTableColumns(t: ComposerTranslation) {
  return [
    { key: 'orderNo', label: t('columns.orderNo'), minWidth: '140px' },
    { key: 'status', label: t('columns.status'), minWidth: '100px' },
    { key: 'createTime', label: t('columns.createTime'), minWidth: '160px' },
  ]
}

使用的时候:

typescript 复制代码
// 组件或 composable 里
const { t } = useI18n()

const statusConfig = createStatusConfig(t)
const columns = computed(() => createTableColumns(t))

为什么这么做?

说实话一开始我也觉得麻烦,直接在模板里写 {{ t('xxx') }} 不香吗?

但仔细想想,配置集中管理的好处太多了:

  • 改列配置不用翻组件代码
  • 多个地方用同一套状态映射,改一处就够了
  • 类型提示友好,拼错 key 立刻报错

而且工厂函数还有个好处------可以保留静态版本做向后兼容

typescript 复制代码
/** @deprecated 请使用 createStatusConfig(t) */
export const STATUS_CONFIG = {
  pending: { text: '待处理', type: 'warning' },
  // ...
}

老代码可以慢慢迁移,不用一口气改完。

方案二:策略模式处理状态映射

业务里有个场景:后端返回一个 timeStatus 字段,可能是 'urgent''normal''overdue',前端要映射成不同的样式和文案。

最初的写法大概是这样:

typescript 复制代码
function getStatusStyle(status: string) {
  if (status === 'urgent') return 'warning'
  if (status === 'normal') return 'success'
  if (status === 'overdue') return 'disabled'
  return 'info'
}

function getStatusText(status: string) {
  if (status === 'urgent') return '即将超时'
  if (status === 'normal') return '正常'
  if (status === 'overdue') return '已超时'
  return '-'
}

两个函数里写了两遍映射逻辑,而且新增状态要改两个地方。

用策略模式重构一下:

typescript 复制代码
// 类型定义
type TimeStatus = 'urgent' | 'normal' | 'overdue'

interface StatusStrategy {
  style: 'warning' | 'success' | 'disabled' | 'info'
  i18nKey: string
}

// 策略映射表
const TIME_STATUS_STRATEGY: Record<TimeStatus, StatusStrategy> = {
  urgent: { style: 'warning', i18nKey: 'status.urgent' },
  normal: { style: 'success', i18nKey: 'status.normal' },
  overdue: { style: 'disabled', i18nKey: 'status.overdue' },
}

// 统一的获取方法
function getTimeStatus(status: string): StatusStrategy {
  return TIME_STATUS_STRATEGY[status as TimeStatus] ?? { 
    style: 'info', 
    i18nKey: 'status.unknown' 
  }
}

现在新增一个状态,只需要在映射表里加一行。

这个模式还有个妙用------处理多 Tab 场景

typescript 复制代码
type TabType = 'pending' | 'processing' | 'completed'

// 每个 Tab 对应不同的列配置
const TAB_COLUMNS_STRATEGY: Record<TabType, () => TableColumn[]> = {
  pending: () => createPendingColumns(t),
  processing: () => createProcessingColumns(t),
  completed: () => createCompletedColumns(t),
}

// 使用
const columns = computed(() => {
  const strategy = TAB_COLUMNS_STRATEGY[activeTab.value]
  return strategy ? strategy() : []
})

比一堆 if-else 清爽多了。

方案三:Composables 的组合复用

Vue 3 的 Composition API 本身就是一种组合模式的体现。但我发现很多人写 composable 的时候,要么写成一个巨大的函数,要么拆得太碎不好用。

这次我的做法是按职责拆分,按场景组合

typescript 复制代码
// composables/useTableConfig.ts
// 只负责表格配置相关
export function useTableConfig(tabRef: Ref<TabType>) {
  const { t } = useI18n()
  
  const columns = computed(() => {
    const columnsMap = createTabColumnsMap(t)
    return columnsMap[tabRef.value] ?? []
  })
  
  return { columns }
}

// composables/useStatusMapper.ts  
// 只负责状态映射
export function useStatusMapper() {
  const { t } = useI18n()
  
  function getStatusDisplay(status: string) {
    const strategy = TIME_STATUS_STRATEGY[status]
    return {
      text: strategy ? t(strategy.i18nKey) : '-',
      type: strategy?.style ?? 'info',
    }
  }
  
  return { getStatusDisplay }
}

// composables/usePageData.ts
// 页面级 composable,组合上面的功能
export function usePageData() {
  const activeTab = ref<TabType>('pending')
  
  // 组合其他 composables
  const { columns } = useTableConfig(activeTab)
  const { getStatusDisplay } = useStatusMapper()
  
  // 页面特有的逻辑
  const tableData = ref([])
  async function loadData() { /* ... */ }
  
  return {
    activeTab,
    columns,
    tableData,
    getStatusDisplay,
    loadData,
  }
}

组合的原则是什么?

我的经验是:

  • 基础 composable:单一职责,不依赖具体业务
  • 业务 composable:组合基础能力,添加业务逻辑
  • 页面 composable:组装一切,对接组件

这样拆的好处是,基础 composable 可以在其他模块复用,而业务逻辑变化只影响上层。

踩过的坑

说几个实际遇到的问题:

1. 工厂函数调用时机

一开始我在模块顶层调用工厂函数:

typescript 复制代码
// ❌ 错误:模块加载时 i18n 还没准备好
const config = createStatusConfig(t)

正确做法是在 setup 里调用,或者用 computed 包装:

typescript 复制代码
// ✅ 正确
const config = computed(() => createStatusConfig(t))

2. 类型推导丢失

策略表如果用 Record<string, xxx> 定义,key 的类型提示就没了:

typescript 复制代码
// ❌ key 是 string,没有提示
const map: Record<string, Strategy> = { ... }

// ✅ 用 as const 或明确 key 类型
const map: Record<'a' | 'b' | 'c', Strategy> = { ... }

3. 向后兼容别忘了

重构不是一锤子买卖。保留老的导出,加上 @deprecated 注释,让调用方有时间迁移:

typescript 复制代码
/** @deprecated 请使用 createXxx() */
export const OLD_CONFIG = { ... }

写在最后

设计模式这东西,学的时候觉得玄乎,用的时候发现也就那样------本质上是前人踩坑踩出来的套路

不用刻意追求"我要用 xx 模式",而是遇到问题的时候想想:

  • 这块逻辑重复了,能不能抽出来?
  • 这个 if-else 越来越长,能不能用映射表?
  • 这个配置老是改,能不能集中管理?

想清楚问题,模式自然就出来了。

希望对你有帮助。有问题欢迎评论区交流。


技术栈:Vue 3.5 + TypeScript 5 + vue-i18n

相关推荐
shenzhenNBA2 小时前
如何在python文件中使用日志功能?简单版本
java·前端·python·日志·log
掘金泥石流2 小时前
分享下我创业烧了 几十万的 AI Coding 经验
前端·javascript·后端
用户47949283569152 小时前
JavaScript 为什么选择原型链?从第一性原理聊聊这个设计
前端·javascript
new code Boy2 小时前
vscode左侧栏图标及目录恢复
前端·javascript
唐诗2 小时前
Git提交信息太乱?AI一键美化!一行命令拯救你的项目历史🚀
前端·ai编程
涔溪2 小时前
有哪些常见的Vite插件及其作用?
前端·vue.js·vite
糖墨夕2 小时前
从一行代码看TypeScript的精准与陷阱:空值合并vs逻辑或
前端·typescript
Junsen2 小时前
使用 Supabase 实现轻量埋点监控
前端·javascript
CnLiang2 小时前
React Compiler Plugin
前端·react.js