很多人第一次看到 enum-plus,第一反应不是"这个方案好不好",而是:
- 老项目已经跑了很久,怎么可能一下子改掉?
- 现在代码里全是
label map、options、filters,迁移成本会不会太高? - 就算我认可"单一数据源",也不想为了这件事搞一次大重构
这些顾虑都很正常。
也是因为这个原因,我一直觉得:enum-plus 真正更适合落地的方式,不是"全量替换",而是"渐进式迁移"。
也就是说,你完全没必要一上来就推翻项目里所有 enum、as const、map、helper。
更现实的做法是:
先挑一个最容易反复出问题的状态字典,把它从"多份平行 map"收敛成一份运行时定义。
这篇文章就专门聊这个过程。
不是讲语法,而是讲:
一个真实后台项目,怎么把状态字典渐进式迁移到 enum-plus?
一、先考虑一个前提:不要上来就全改
如果你的项目已经在线运行,里面到处都是这些东西:
statusLabelMapstatusColorMapstatusOptionsstatusFiltersstatusValueEnumgetStatusText()isFinalStatus()
那说明问题确实存在。
但这并不意味着你该立刻:
- 全项目搜索替换
- 一次性重写所有状态模块
- 在一个版本里同时改表格、表单、筛选器、国际化
为什么我不建议这么做?
因为这种改法通常会带来 3 个风险:
- 改动面太大,验证成本太高
- 老页面兼容逻辑容易漏
- 团队会把"字典治理"理解成一次高风险重构
而一旦大家把这件事和"大重构"绑定在一起,推进就会变得非常困难。
所以更现实的策略是:
小范围试点、逐步替换、兼容过渡、最后收尾。
二、先找"最值得迁移"的那一组状态
不是所有状态值都值得动。
如果只有 2 个值,只在一个文件里判断一次,那继续用 as const 完全没问题。
真正值得优先迁移的,通常有这些特征:
1)同一个状态会出现在多个 UI 位置
比如订单状态同时出现在:
- 列表页文案
- 详情页标签
- 查询表单下拉框
- 表格 filters
valueEnum- 国际化文案
2)现在已经维护了多份平行结构
比如你已经有:
orderStatusLabelMaporderStatusColorMaporderStatusOptionsorderStatusFiltersorderStatusValueEnum
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',
},
});
请注意,这一步的重点不是立刻改所有页面。
重点是先建立一个新的单一数据源。
也就是说,迁移第一阶段的目标应该是:
让"后续所有新的文案、颜色、选项、过滤器逻辑"都优先从新字典里拿。
先把源头立起来,再逐步替换调用方,这样节奏会稳很多。
五、第三步:优先替换"读操作",而不是"写结构"
这一步是我觉得最关键的迁移技巧。
很多人一上来就想把原来的:
optionsfiltersvalueEnum- 各种 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)普通组件
有些场景直接复用 items 或 toList() 就够了:
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 也还在
- 页面一部分用新方式,一部分还用旧方式
结果时间一长,项目反而进入"双轨并存"的尴尬状态。
所以我建议你给每一组迁移都设一个明确的收尾动作:
收尾清单
- 所有文本展示都改为
enum.label(...) - 所有颜色读取都改为
enum.raw(...).color - 所有 options / filters / valueEnum 都来自统一字典或插件方法
- 旧
labelMap/colorMap/options/filters不再被引用 - 删除兼容导出
- 搜索确认旧 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 mapcolor mapoptionsfiltersvalueEnum- i18n key
那我觉得你完全可以先挑一组最典型的状态,试一次。
不要想着一次性改变整个项目。
只要你能先把一组状态从"平行 map"收拢成"单一数据源",后面的收益通常会越来越明显。
GitHub:github.com/shijistar/e...
这个项目我还在持续打磨,也会继续把踩坑、设计取舍和实践案例写出来。 如果你希望我把这条线继续做深,欢迎到 GitHub 支持一个 Star。