一个配置页面里七八组下拉选项全是写死的常量,甚至拿中文当业务值传给后端。本文记录一次完整的「字典化改造」实践:从风险盘点、逐项替换、联动过滤到请求合并,附踩坑与收益复盘。
背景:一个"全是硬编码"的配置页
最近在改造某业务系统里的一个配置管理页面。功能本身不复杂:规则列表 + 详情页双 Tab(阈值配置、参数配置),里面散布着设备类型、供应商、覆盖等级、参数类型、运行模式、开关状态等七八组下拉框。
动手前做了一次代码体检,结果触目惊心:
- 整个目录完全没有使用字典系统 ------
useDictionaryStore、loadDictionaries、useOptions、getLabel一概没有; - 所有下拉选项都在
constants.ts里硬编码; - 部分选项甚至直接用中文作为业务值(比如"模式一"直接作为提交值);
- 而同系统的其他页面早就规范地使用了字典,唯独这个目录是"漏网之鱼"。
这不是代码风格问题,而是实打实的隐患。我们逐一来说。
硬编码的四个坑
1. 中文当业务值,前后端迟早打架
把"模式一"这样的中文直接作为 value 提交给后端,一旦后端迁移、国际化或者接口规范统一,全链路都要跟着遭殃。字典系统的核心价值就是值域与展示分离:接口传编码,页面显文案。
2. 后端数据不兼容
后端如果按字典编码下发给前端(比如 device_type: "C"),而页面选项还是写死的 A、B,就会出现"下拉框里选不到后端返回的值"这种诡异 Bug。
3. 配置不生效
很多企业项目会把字典做成可配置数据(后台可增删选项)。硬编码意味着后台加了新供应商、新参数类型,前端永远看不到------配置形同虚设。
4. 业务扩展困难
每次新增选项都要改代码、走发版,完全丧失了配置驱动的灵活性。
字典化改造:一张表看懂替换方案
改造的第一步,是把硬编码常量与"应使用的字典/公共方案"一一对应起来:
| 原硬编码选项 | 替换方案 |
|---|---|
DEVICE_TYPE_OPTIONS |
device_type 字典 |
VENDOR_OPTIONS |
vendor_name 字典 |
COVERAGE_LEVEL_OPTIONS |
coverage_level_full 字典 |
PARAM_TYPE_OPTIONS |
param_type 字典 |
HOUR_OPTIONS |
hour_of_day 字典(0-23 时) |
MODE_TYPE_OPTIONS |
复用项目公共的运行模式获取函数 |
REGION_OPTIONS |
复用 useRegionOptions 公共 hook |
SWITCH_OPTIONS |
复用 SWITCH_STATUS_OPTIONS |
几个关键决策点:
能复用公共能力就绝不自造。 区域选项、运行模式、开关状态这些"别人已经封装好"的东西,直接切到公共 hook / 公共常量,既减少维护面,也保证全站行为一致。
选字典要对照页面语义。 覆盖等级这个坑值得单独说:项目里 coverage_level 只有两级,和页面的语义对不上,而 coverage_level_full 才是详细分类------如果只看名字随手选,就会选错。字典名和页面语义对不上时,优先参考同系统其他页面的用法。
值域约定跟着字典走。 开关状态从原来的 'ON'/'OFF' 字符串切换成字典的 0/1 数值后,提交值和后端字段约定一起变了,这属于"字典化"必须连带处理的隐性改动,改完要回头检查类型定义和 Mock 数据是否同步。
联动过滤:设备类型 → 运行模式
字典化只是第一步。改造中发现:运行模式的可选范围取决于当前设备类型,这是典型的级联下拉场景。
核心逻辑用 computed + watch 实现:
ts
// 根据当前选中的设备类型,过滤出可选的运行模式
const modeOptions = computed(() => {
// 设备类型为空 → 返回全量;否则按类型过滤
return getModeOptionsByDeviceType(queryState.deviceType);
});
// 切换设备类型时,若当前运行模式已不在新列表内,自动清空/落到第一个
watch(() => queryState.deviceType, (val) => {
const options = modeOptions.value;
if (!options.some((o) => o.value === queryState.mode)) {
queryState.mode = options[0]?.value;
}
});
这里最容易被忽视的是场景差异:
- 查询栏 :设备类型为空时,运行模式全量展示(用户可以不选设备类型直接查);
- 新增/编辑表单 :设备类型为空时,运行模式应为空(避免用户提交一个没有归属类型的运行模式)。
同一个"联动"逻辑,在查询和表单里行为完全相反。如果只做一版,就必然有一处是错的。所以改造时要逐场景确认:
- 查询栏设备类型为空 → 运行模式全量展示
- 表单设备类型为空 → 运行模式无选项、不可勾选
- 切换设备类型 → 自动清空失效的运行模式
- 用户手动清空运行模式 → 保持为空,不要被 watch 强行填回默认值
另外一个细节:运行模式的选项接口可能是异步加载的,选项数据"迟到"时 watch 要在数据到达后自动补默认值,否则页面一打开就是空的。
请求优化:5 个并发请求合并为 1 个
字典化之后,页面从"写死常量"变成了"按需拉字典"。初期实现是按字典类型逐个请求:
ts
// 改造前:5 个字典类型 = 5 个请求
const deviceTypeOptions = useDictionaryOptions('device_type');
const vendorOptions = useDictionaryOptions('vendor_name');
const coverageOptions = useDictionaryOptions('coverage_level_full');
// ...
页面实际用到 5 种字典类型,打开一次就是 5 个并发请求,性能不优雅,也容易被后端盯着看。项目里其实已经存在一个支持批量获取的字典 Store,只是这个页面没用上。改造后:
ts
// 改造后:一次批量请求搞定
await dictStore.loadDictionaries([
'device_type',
'vendor_name',
'coverage_level_full',
'param_type',
'hour_of_day',
]);
const deviceTypeOptions = dictStore.useOptions('device_type');
收益不只是"5 变 1":
- Store 内部有 in-flight 合并 (同类型同时被请求时只发一次)和已加载去重(拉过的不再拉),后续其他页面复用成本为零;
useOptions返回的 computed 在模板里自动解包,下拉组件的绑定一行都不用改;- 字典加载前后的响应式更新,不影响既有的联动逻辑和默认值 watch;
- 字典加载和数据列表请求可以并行发起,互不依赖,首屏更快。
这类"顺手优化"在改造中最容易被忽略,但对线上性能是实打实的收益。
质量保障:把 pre-commit 钩子当最后一道防线
改造全程靠三件套兜底:vue-tsc -b 全量类型检查、ESLint、stylelint。
这里有一个真实的翻车案例:某次提交代码时 git commit 直接失败,pre-commit 钩子报错------排查发现是模板里多写了一个闭合标签 ,template 结构不合法。prettier --write 和 lint 修复命令也相继被终止。
这个教训值得写进团队规范:
- 钩子报错先看语法,再看 lint------模板结构错误会阻断后续所有自动化;
- 类型检查要跑全量 (
vue-tsc -b),只盯单文件很容易漏掉跨文件引用; - 改完选项类改动,务必检查类型定义和 Mock 数据 是否同步更新------字典化常伴随值域约定变更(如
'ON'/'OFF'→0/1),漏一处就是运行时 Bug。
结语
这次改造的最终收益可以总结为三句话:
- 值域统一:所有下拉选项收口到字典/公共能力,中文当业务值的隐患清零;
- 配置驱动:后台增删选项前端即时生效,业务扩展不再依赖发版;
- 体验优化:联动过滤 + 批量请求,交互更合理、首屏请求数从 5 降到 1。
整个过程没有什么高深技巧,但"盘点 → 对照 → 替换 → 联动 → 优化 → 质检"这个流程,几乎是所有企业项目做选项治理的通用范式。如果你手里也有一个"全是硬编码"的老页面,不妨照着这个清单过一遍------收益远比想象中大。
如果你所在的项目也在做类似的字典化或前端治理改造,欢迎在评论区聊聊你们踩过的坑。