聊聊设计模式在 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

相关推荐
xiaofeichaichai1 小时前
Webpack
前端·webpack·node.js
Thecozzy1 小时前
线上 Bug 排查与修复实录
架构
鹏大师运维1 小时前
为什么信创电脑装软件总提示“软件包架构不匹配”?
linux·运维·架构·国产化·麒麟·deb·统信uos
问心无愧05131 小时前
ctf show web入门111
android·前端·笔记
唐某人丶1 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界2 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌2 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
excel3 小时前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia3113 小时前
https连接传输流程
前端·面试