别再一次性重构枚举了:如何把一个真实后台项目的状态字典,渐进式迁移到enum-plus?

很多人第一次看到 enum-plus,第一反应不是"这个方案好不好",而是:

  • 老项目已经跑了很久,怎么可能一下子改掉?
  • 现在代码里全是 label mapoptionsfilters,迁移成本会不会太高?
  • 就算我认可"单一数据源",也不想为了这件事搞一次大重构

这些顾虑都很正常。

也是因为这个原因,我一直觉得:enum-plus 真正更适合落地的方式,不是"全量替换",而是"渐进式迁移"。

也就是说,你完全没必要一上来就推翻项目里所有 enumas const、map、helper。

更现实的做法是:

先挑一个最容易反复出问题的状态字典,把它从"多份平行 map"收敛成一份运行时定义。

这篇文章就专门聊这个过程。

不是讲语法,而是讲:

一个真实后台项目,怎么把状态字典渐进式迁移到 enum-plus?


一、先考虑一个前提:不要上来就全改

如果你的项目已经在线运行,里面到处都是这些东西:

  • statusLabelMap
  • statusColorMap
  • statusOptions
  • statusFilters
  • statusValueEnum
  • getStatusText()
  • isFinalStatus()

那说明问题确实存在。

但这并不意味着你该立刻:

  • 全项目搜索替换
  • 一次性重写所有状态模块
  • 在一个版本里同时改表格、表单、筛选器、国际化

为什么我不建议这么做?

因为这种改法通常会带来 3 个风险:

  1. 改动面太大,验证成本太高
  2. 老页面兼容逻辑容易漏
  3. 团队会把"字典治理"理解成一次高风险重构

而一旦大家把这件事和"大重构"绑定在一起,推进就会变得非常困难。

所以更现实的策略是:

小范围试点、逐步替换、兼容过渡、最后收尾。


二、先找"最值得迁移"的那一组状态

不是所有状态值都值得动。

如果只有 2 个值,只在一个文件里判断一次,那继续用 as const 完全没问题。

真正值得优先迁移的,通常有这些特征:

1)同一个状态会出现在多个 UI 位置

比如订单状态同时出现在:

  • 列表页文案
  • 详情页标签
  • 查询表单下拉框
  • 表格 filters
  • valueEnum
  • 国际化文案

2)现在已经维护了多份平行结构

比如你已经有:

  • orderStatusLabelMap
  • orderStatusColorMap
  • orderStatusOptions
  • orderStatusFilters
  • orderStatusValueEnum

3)这组状态后面还会继续长

比如还会不断新增:

  • 新状态
  • 新文案
  • 新颜色
  • 新图标
  • 新权限字段

如果你满足上面任意两条,这组状态大概率就很适合拿来做第一批试点。

我的建议是:

先从"最痛的一组状态"开始,不要从"全局最重要"的模块开始。

因为试点的目标不是证明你能重构全项目,而是证明:

  • 这套模式是可行的
  • 迁移风险可控
  • 后续值得推广

三、先把现有"平行 map"摊开,不要急着改

很多人一开始就直接写新代码,这其实很容易漏东西。

更稳妥的方式是先做一次盘点。

假设你现在项目里有这样的代码:

ts 复制代码
enum OrderStatus {
  Pending = 0,
  Paid = 1,
  Refunded = 2,
}

export const orderStatusLabelMap = {
  [OrderStatus.Pending]: '待支付',
  [OrderStatus.Paid]: '已支付',
  [OrderStatus.Refunded]: '已退款',
};

export const orderStatusColorMap = {
  [OrderStatus.Pending]: 'orange',
  [OrderStatus.Paid]: 'green',
  [OrderStatus.Refunded]: 'red',
};

export const orderStatusOptions = [
  { value: OrderStatus.Pending, label: '待支付' },
  { value: OrderStatus.Paid, label: '已支付' },
  { value: OrderStatus.Refunded, label: '已退款' },
];

这一步先别急着删。

你真正该做的是把这些分散信息整理成一张表:

key value label color 还在哪些地方被用到
Pending 0 待支付 orange table / filter / select
Paid 1 已支付 green table / filter / detail
Refunded 2 已退款 red detail / finance / export

这个动作很朴素,但很重要。

因为它能帮你明确:

  • 当前到底有哪些运行时信息
  • 有没有互相冲突的定义
  • 迁移后应该沉淀成哪些字段

很多项目迁移失败,不是因为工具不行,而是因为原来那堆平行 map 本身就已经不一致了


四、第二步:先把"数据源"收拢,再考虑替换调用方

当你把现有信息盘清楚之后,再定义新的枚举字典。

ts 复制代码
import { Enum } from 'enum-plus';

export const OrderStatus = Enum({
  Pending: {
    value: 0,
    label: '待支付',
    color: 'orange',
    icon: 'clock-circle',
  },
  Paid: {
    value: 1,
    label: '已支付',
    color: 'green',
    icon: 'check-circle',
  },
  Refunded: {
    value: 2,
    label: '已退款',
    color: 'red',
    icon: 'close-circle',
  },
});

请注意,这一步的重点不是立刻改所有页面。

重点是先建立一个新的单一数据源。

也就是说,迁移第一阶段的目标应该是:

让"后续所有新的文案、颜色、选项、过滤器逻辑"都优先从新字典里拿。

先把源头立起来,再逐步替换调用方,这样节奏会稳很多。


五、第三步:优先替换"读操作",而不是"写结构"

这一步是我觉得最关键的迁移技巧。

很多人一上来就想把原来的:

  • options
  • filters
  • valueEnum
  • 各种 helper

全部删掉重写。

但更稳妥的顺序其实是:先替换最简单、最容易验证的读取场景

比如:

1)文本展示

ts 复制代码
OrderStatus.label(row.status)

2)读取元数据

ts 复制代码
OrderStatus.raw(row.status).color
OrderStatus.named.Paid.raw.icon

3)反查 key

ts 复制代码
OrderStatus.key(1) // 'Paid'

4)遍历列表

ts 复制代码
OrderStatus.items
OrderStatus.toList()
OrderStatus.toMap()

这些改动通常风险更低,因为它们大多只是把原来"从 map 里取值"的地方,改成"从统一字典里取值"。

而且非常容易肉眼验证:

  • 文案对不对
  • 颜色对不对
  • 下拉列表对不对

这会让团队很快看到收益,但又不会一下子引入太多不确定性。


六、第四步:保留兼容层,别急着删旧接口

如果你的项目比较大,我非常建议在过渡期保留兼容层。

比如,旧页面还在用:

ts 复制代码
orderStatusLabelMap
orderStatusOptions

那你完全可以先这样过渡:

ts 复制代码
export const orderStatusLabelMap = OrderStatus.toMap();
export const orderStatusOptions = OrderStatus.toList();

如果原来有旧 helper,也可以先桥接:

ts 复制代码
export const getOrderStatusText = (value: number) => OrderStatus.label(value);
export const getOrderStatusColor = (value: number) => OrderStatus.raw(value).color;

这一步的价值非常大。

因为它意味着:

  • 调用方可以先不全部改
  • 你先把"数据源"统一掉
  • 旧模块还能继续工作

这其实就是典型的"先收口源头,再渐进收口出口"。

在我看来,很多重构之所以难,不是因为代码改不了,而是因为大家总想把"新数据结构"和"所有旧调用方"在同一时间一起清掉。

这往往没有必要。


七、第五步:再逐步迁移 UI 生成逻辑

当文本和基础元数据都稳定之后,再考虑把 UI 生成逻辑也收进来。

1)普通组件

有些场景直接复用 itemstoList() 就够了:

tsx 复制代码
<Select options={OrderStatus.items} />
// 或
<Select options={OrderStatus.toList()} />

2)Ant Design / ProComponents

如果你在用 v3 的插件体系,可以安装:

bash 复制代码
npm install @enum-plus/plugin-antd

然后在入口安装:

ts 复制代码
import antdPlugin from '@enum-plus/plugin-antd';
import { Enum } from 'enum-plus';

Enum.install(antdPlugin);

之后你就可以直接生成:

  • OrderStatus.toSelect()
  • OrderStatus.toMenu()
  • OrderStatus.toFilter()
  • OrderStatus.toValueMap()

也就是说,原来很多散落在表格、表单配置里的映射逻辑,就可以慢慢回收到统一字典之上。

这一步不一定要最先做,但当你完成到这里时,迁移收益通常就已经非常明显了。


八、第六步:如果有国际化,再最后接 i18n

国际化通常是最适合放在后面的。

因为它往往牵涉:

  • 文案 key 命名
  • 多语言资源文件
  • React / Vue 组件刷新
  • 页面级联验证

enum-plus 里,这部分也有两种路径:

路径 1:使用插件

比如:

bash 复制代码
npm install @enum-plus/plugin-i18next i18next

然后:

ts 复制代码
import i18nextPlugin from '@enum-plus/plugin-i18next';
import { Enum } from 'enum-plus';

Enum.install(i18nextPlugin);

此时你可以把 label 写成 i18n key。

路径 2:直接覆写 Enum.localize

如果你项目里不是 i18next,也可以自己接:

ts 复制代码
import { Enum } from 'enum-plus';

Enum.localize = (key) => {
  return intl.formatMessage({ id: key });
};

我建议把 i18n 放到迁移后半段,不要一开始就和字典重构绑死。

这样更容易定位问题,也更容易分阶段上线。


九、第七步:等新路径稳定后,再删除旧 map

这一步看起来最简单,但其实最考验纪律。

因为很多团队做到一半会停在这里:

  • 新字典有了
  • 老 map 也还在
  • 页面一部分用新方式,一部分还用旧方式

结果时间一长,项目反而进入"双轨并存"的尴尬状态。

所以我建议你给每一组迁移都设一个明确的收尾动作:

收尾清单

  1. 所有文本展示都改为 enum.label(...)
  2. 所有颜色读取都改为 enum.raw(...).color
  3. 所有 options / filters / valueEnum 都来自统一字典或插件方法
  4. labelMap / colorMap / options / filters 不再被引用
  5. 删除兼容导出
  6. 搜索确认旧 helper 没有残留调用

只有走到这一步,这次迁移才算真正完成。


十、一个我更推荐的迁移节奏

如果让我给一个最实用的顺序,我会推荐这样做:

第 1 周:先选试点状态字典

  • 选一组最痛的状态
  • 盘点现有 map 和 helper
  • 建立新的 Enum(...) 定义

第 2 周:先替换读操作

  • 文本展示
  • 颜色读取
  • key / label 反查
  • 下拉列表基本输出

第 3 周:补 UI 生成和插件接入

  • toList() / toMap()
  • plugin-antd
  • 表格 filters / valueEnum / 菜单

第 4 周:做清理和收口

  • 删除旧 map
  • 删除兼容 helper
  • 补文档
  • 固化约定

这种推进方式的好处是:

  • 每一步都容易验证
  • 每一步都能看到收益
  • 每一步都可以独立回滚

它比"一次性大重构"更像真正能在业务团队里落地的方式。


十一、最后总结

我越来越觉得,enum-plus 更适合被理解成一种前端运行时业务字典的组织方式,而不是一次"替换所有 enum 的运动"。

真正现实的落地方式不是:

  • 全量替换
  • 一次性重写
  • 把所有旧代码推翻

而是:

先挑一个最痛的状态字典,先统一数据源,再渐进式替换读取,再接 UI 生成,最后收口旧 map。

如果你正在做的也是中后台项目,而且项目里已经有很多:

  • label map
  • color map
  • options
  • filters
  • valueEnum
  • i18n key

那我觉得你完全可以先挑一组最典型的状态,试一次。

不要想着一次性改变整个项目。

只要你能先把一组状态从"平行 map"收拢成"单一数据源",后面的收益通常会越来越明显。

GitHub:github.com/shijistar/e...

这个项目我还在持续打磨,也会继续把踩坑、设计取舍和实践案例写出来。 如果你希望我把这条线继续做深,欢迎到 GitHub 支持一个 Star。

相关推荐
暗不需求1 小时前
React 性能优化秘籍:深入理解 `useMemo` 与 `useCallback`
前端·react.js·面试
专注VB编程开发20年2 小时前
我制作excel工作簿的选项卡,发给deep seek, 昨天修改了一天
前端·vue.js·excel
light blue bird2 小时前
工序路径主子表单工序组装图表组件
前端·数据库·信息可视化·.net·web端·razor page
java_cj2 小时前
MySQL 8.0新特性详解:从隐藏索引到窗口函数全面解析
数据库·mysql·架构·开源
linlinlove22 小时前
前端uniapp、后端thinkphp股票系统开发功能展示、代码披露、HQChart
前端·uni-app·echarts·thinkphp·hqchart·配资·deepseek选股票
万少2 小时前
Claude Code 任务结束会自己喊你:一个 Stop Hook 搞定提示音
前端·后端·代码规范
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_30:(玩转列表样式,从基础到进阶)
前端·css·html·tensorflow·媒体
追光者♂2 小时前
【测评系列3】CSDN AI数字营销实测体验官:测试 开源项目——Superpowers 游戏引擎从零开发实战指南
人工智能·深度学习·机器学习·typescript·开源·游戏引擎·superpowers
ct9782 小时前
TypeScript 中的泛型
前端·javascript·typescript