【前端常量管理 + Vue/TS 项目】:从核心规范到落地实操,彻底搞懂业务常量/枚举/字典的最佳写法,避开前后端状态不一致、多份定义、维护难高频坑!
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、先看一段“背锅代码”
先从一个真实到不能再真实的例子开始:
// 某个订单列表页
if (order.status === 1) {
showPayButton = true;
}
if (order.status === 3) {
showCancelButton = false;
}
刚写完的时候你可能记得:
1代表“待支付”3代表“已取消”
但是问题来了:
- 过了两周,你还记得吗?
- 新同事看这段代码,会知道
1和3是啥吗? - 产品突然说:“未支付状态从 1 改成 10”——你准备怎么改?
这段代码至少有三个问题:
- 可读性差:
1、3完全没有语义,靠“记忆”来理解。 - 难维护:要改状态值,只能全局搜索
1和3,非常不安全。 - 易出 Bug:复制粘贴时多打一位、少打一位都不报错,只会在业务上静悄悄出事。
改造一下:
// 常量集中管理(order.constants.ts)
export const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
CANCELED: 3,
} as const;
// 业务代码
if (order.status === ORDER_STATUS.UNPAID) {
showPayButton = true;
}
if (order.status === ORDER_STATUS.CANCELED) {
showCancelButton = false;
}
这样就立刻好很多:
- 读得懂:一眼就知道
UNPAID、CANCELED是什么状态。 - 好维护:状态码改了,只改一个地方。
- 好搜索:全局搜
ORDER_STATUS.UNPAID精准且安全。
一句话总结:常量不统一管理,本质上是在项目里埋地雷。
二、什么是“魔法数字 / 魔法字符串”?为什么要拒绝?
魔法数字 / 魔法字符串(Magic Number / Magic String) 指的是:
在代码里直接写的“裸数字 / 生字符串”,却没有任何语义说明。
2.1 典型反例
// 角色
if (user.role === 1) {
// 管理员
}
if (user.role === 2) {
// 普通用户
}
// 订单类型
if (order.type === 'VIP_001') {
// VIP 订单
}
if (order.type === 'VIP_002') {
// 超级 VIP 订单
}
存在的问题:
- 看不懂:
1、2、VIP_001是啥?只有当事人知道。 - 不统一:有的人写
'vip_001',有的人写'VIP_001',大小写一个不注意就错。 - 难修改:要改时你只能全局搜
'VIP_001',而文本、文案里也可能有这个字符串,容易误改。
2.2 正确姿势:抽成常量 / 枚举
// user.constants.ts
export const USER_ROLE = {
ADMIN: 1,
NORMAL: 2,
} as const;
// order.constants.ts
export const ORDER_TYPE = {
VIP: 'VIP_001',
SUPER_VIP: 'VIP_002',
} as const;
// 使用
if (user.role === USER_ROLE.ADMIN) {
// ...
}
if (order.type === ORDER_TYPE.VIP) {
// ...
}
目标是:业务代码中基本看不到“裸数字 / 生字符串”,而是有语义的常量名。
三、在 Vue 项目里,常量到底应该放哪儿?
规范落不到文件结构上,都只能停留在嘴上。先定一个“可以直接照抄”的组织方式。
3.1 推荐目录结构
src
├── constants
│ ├── common.ts // 通用常量(YES/NO、环境、通用状态等)
│ ├── user.ts // 用户相关常量(角色、权限)
│ ├── order.ts // 订单相关常量(状态、类型)
│ └── index.ts // 统一出口(可选)
├── utils
│ └── enums.ts // 辅助函数,如生成 options 等
└── pages
└── order
└── OrderList.vue
基本原则:
-
按业务域拆文件:
- 用户相关丢到
user.ts - 订单相关丢到
order.ts
- 用户相关丢到
-
所有业务常量统一集中在
src/constants下,而不是散落在各个组件里。 -
Vue 组件只负责“用”,不负责“定义这类可复用的业务常量”。
3.2 一个完整的订单常量示例(可直接照搬)
// src/constants/order.ts
// 订单状态枚举值(跟后端 / 数据库一致)
export const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
SHIPPED: 3,
COMPLETED: 4,
CANCELED: 5,
} as const;
// 如果用 TypeScript,可以推导出类型:
export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS];
// 状态对应的展示文案
export const ORDER_STATUS_LABEL = {
[ORDER_STATUS.UNPAID]: '待支付',
[ORDER_STATUS.PAID]: '已支付',
[ORDER_STATUS.SHIPPED]: '已发货',
[ORDER_STATUS.COMPLETED]: '已完成',
[ORDER_STATUS.CANCELED]: '已取消',
} as const;
// 状态对应的样式/颜色(比如配合 Element Plus 的 Tag)
export const ORDER_STATUS_COLOR = {
[ORDER_STATUS.UNPAID]: 'warning',
[ORDER_STATUS.PAID]: 'success',
[ORDER_STATUS.SHIPPED]: 'info',
[ORDER_STATUS.COMPLETED]: 'success',
[ORDER_STATUS.CANCELED]: 'danger',
} as const;
在 Vue 组件中使用:
<!-- src/pages/order/OrderList.vue -->
<script setup lang="ts">
import {
ORDER_STATUS,
ORDER_STATUS_LABEL,
ORDER_STATUS_COLOR,
} from '@/constants/order';
const isUnpaid = (order: any) => order.status === ORDER_STATUS.UNPAID;
const orderList = ref([
{ id: 'A001', status: ORDER_STATUS.UNPAID },
{ id: 'A002', status: ORDER_STATUS.PAID },
]);
</script>
<template>
<div>
<el-table :data="orderList">
<el-table-column prop="id" label="订单号" />
<el-table-column label="订单状态">
<template #default="{ row }">
<el-tag :type="ORDER_STATUS_COLOR[row.status]">
{{ ORDER_STATUS_LABEL[row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button
v-if="isUnpaid(row)"
type="primary"
>
去支付
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
你会发现:
- 整个组件里看不到任何裸数字,只看到有语义的常量名。
- 状态文案、颜色、逻辑判断都是围绕同一套常量转。
- 以后需求变更,你主要改的是常量文件,而不是到处翻组件。
四、枚举到底怎么选?JS / TS / Vue 的几种写法
很多同学提到“枚举”,第一反应就是 enum 关键字。但在前端项目里,其实有几种不同实践方式。
4.1 纯 JS 项目(无 TypeScript)
推荐写法:const + 对象 + as const(可选)
如果你是纯 JS 项目,可以这样写(as const 可省略,仅 TS 有用):
// src/constants/common.ts
export const YES_NO = {
YES: 1,
NO: 0,
} as const;
export const GENDER = {
MALE: 'M',
FEMALE: 'F',
} as const;
使用:
import { YES_NO, GENDER } from '@/constants/common';
if (form.isActive === YES_NO.YES) {
// ...
}
if (user.gender === GENDER.MALE) {
// ...
}
优点:
- 写法简单,完全就是普通对象。
- 不依赖任何 TS 特性,纯 JS 项目也通用。
- IDE 自动补全常量名,比自己敲字符串安全多了。
4.2 TypeScript 项目:enum vs as const
写法一:enum(传统枚举)
// order.constants.ts
export enum OrderStatusEnum {
UNPAID = 1,
PAID = 2,
SHIPPED = 3,
}
使用:
if (order.status === OrderStatusEnum.UNPAID) {
// ...
}
优点:
- 语义非常直观,“这就是一个枚举类型”。
缺点(在前端项目里经常被吐槽):
- 编译后的 JS 会额外生成一个双向映射的对象,有些团队不喜欢这种额外代码。
- 和 JSON / 接口交互时,
enum有时候没有对象 + 字面量那么自然。
写法二:推荐的写法:对象 + as const + 类型推导
// order.constants.ts
export const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
SHIPPED: 3,
} as const;
export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS];
使用:
const handleOrder = (status: OrderStatus) => {
if (status === ORDER_STATUS.UNPAID) {
// ...
}
};
优点:
- 编译后就是一个普通对象,运行时简单清晰。
- 和前端常见的“字典对象”“options 数组”配合非常自然。
- 同时兼顾“常量管理”和“类型安全”。
简化记忆:
- 前端 TS 项目,优先考虑“对象 + as const”方案;
- 除非团队统一使用
enum,否则不要同时混用两种风格。
五、状态字典(status map):列表、表单的通用套路
有了“枚举值”,我们通常还需要一整套“围绕枚举值的字典”:文案、颜色、下拉选项等等。
5.1 列表页:状态枚举 + 文案 + 下拉 options
// src/constants/order.ts
export const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
SHIPPED: 3,
COMPLETED: 4,
CANCELED: 5,
} as const;
export const ORDER_STATUS_LABEL = {
[ORDER_STATUS.UNPAID]: '待支付',
[ORDER_STATUS.PAID]: '已支付',
[ORDER_STATUS.SHIPPED]: '已发货',
[ORDER_STATUS.COMPLETED]: '已完成',
[ORDER_STATUS.CANCELED]: '已取消',
} as const;
// 用于下拉筛选的 options
export const ORDER_STATUS_OPTIONS = Object.entries(ORDER_STATUS).map(([_, value]) => ({
label: ORDER_STATUS_LABEL[value as number],
value,
}));
Vue 组件使用示例:
<script setup lang="ts">
import {
ORDER_STATUS,
ORDER_STATUS_LABEL,
ORDER_STATUS_OPTIONS,
} from '@/constants/order';
const searchForm = ref({
status: undefined,
});
const orderList = ref([]);
</script>
<template>
<div>
<!-- 筛选区 -->
<el-form :model="searchForm" inline>
<el-form-item label="订单状态">
<el-select v-model="searchForm.status" clearable placeholder="全部">
<el-option
v-for="item in ORDER_STATUS_OPTIONS"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<!-- 列表 -->
<el-table :data="orderList">
<el-table-column prop="id" label="订单号" />
<el-table-column label="订单状态">
<template #default="{ row }">
{{ ORDER_STATUS_LABEL[row.status] }}
</template>
</el-table-column>
</el-table>
</div>
</template>
好处:
- 状态码、展示文案、筛选下拉都由同一份常量驱动,不会手抄多份。
- 列表、详情、弹窗等多个页面的状态展示保持一致。
- 新增状态时,只要在常量文件里加一条,其他地方基本不用改逻辑。
六、项目要做多语言(i18n),常量怎么设计?
当项目需要支持多语言时,一个常见问题是:常量要不要直接写中文?
建议是:
- 常量只负责“结构 和 key”,具体文案交给 i18n 管。
例如:
// src/constants/order.ts
export const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
SHIPPED: 3,
COMPLETED: 4,
CANCELED: 5,
} as const;
// 文案对应的 i18n key
export const ORDER_STATUS_I18N_KEY = {
[ORDER_STATUS.UNPAID]: 'order.status.unpaid',
[ORDER_STATUS.PAID]: 'order.status.paid',
[ORDER_STATUS.SHIPPED]: 'order.status.shipped',
[ORDER_STATUS.COMPLETED]: 'order.status.completed',
[ORDER_STATUS.CANCELED]: 'order.status.canceled',
} as const;
Vue 组件里:
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { ORDER_STATUS_I18N_KEY } from '@/constants/order';
const { t } = useI18n();
const orderList = ref([]);
</script>
<template>
<el-table :data="orderList">
<el-table-column prop="id" label="订单号" />
<el-table-column :label="t('order.status.title')">
<template #default="{ row }">
{{ t(ORDER_STATUS_I18N_KEY[row.status]) }}
</template>
</el-table-column>
</el-table>
</template>
这样设计的好处:
- 需要多语言时,只改语言包,不动业务逻辑。
- 常量层不混入中文 / 英文等具体文案,更干净。
七、真实项目里常见的坑,以及对应规范
这一节是“踩坑经验”部分,更贴近日常开发。
7.1 前后端状态码不一致
典型场景:
- 数据库
status字段:1=未支付,2=已支付,3=已取消 - 后端用的是这套码值。
- 前端手快自己随便定义了一套:
export const ORDER_STATUS = {
UNPAID: 0, // ❌ 和后端不一致
PAID: 1,
CANCELED: 2,
};
结果:
- 页面展示状态错乱。
- 按钮逻辑错乱。
- 前后端互相甩锅:“你前端写错了”“不,是你后端文档没写清楚”。
规范建议:
- 状态码以 后端 / 产品给出的“码表文档”为准。
- 前端定义常量时,严格按照码表抄写,而不是自己另起炉灶。
- 最好在项目文档里有一份统一的“状态码说明”,前后端共用。
7.2 同一个含义,被定义了多份常量
// A 文件
export const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
};
// B 文件
export const ORDER_STATUS = {
UNPAID: 'UNPAID',
PAID: 'PAID',
};
后果:
- 别的同事 import 时根本不知道该用哪个。
- 两边慢慢演化,定义越来越不一样。
- 极端情况下还可能出现“循环依赖”。
规范建议:
-
按业务域划分常量文件:
user.ts、order.ts、product.ts…… -
每种业务实体,只允许有一个“权威来源”的常量文件。
-
如果需要多个衍生层次,用命名区分:
ORDER_STATUS:具体码值ORDER_STATUS_LABEL:码值 → 文案ORDER_STATUS_COLOR:码值 → 样式ORDER_STATUS_I18N_KEY:码值 → i18n key
7.3 把常量写在组件内部
<script setup lang="ts">
// ❌ 不推荐:写在组件内部的“可复用业务常量”
const ORDER_STATUS = {
UNPAID: 1,
PAID: 2,
};
</script>
问题:
- 另一个组件要用同样的定义,只能复制粘贴。
- 后面某天改值,只改了其中一个组件,悄悄就出现了不一致。
规范建议:
- 所有可复用的业务常量,都应该放到
src/constants目录。 - 组件内部只允许定义“该组件独有且不太可能被复用的小常量”。
八、命名规范 & 实战模板(可直接复制)
8.1 命名规范建议
-
常量对象(值本身):全大写 + 下划线
ORDER_STATUSUSER_ROLEYES_NO
-
字典对象(基于枚举再衍生的映射):在后面加功能后缀
ORDER_STATUS_LABELORDER_STATUS_COLORUSER_ROLE_OPTIONS
-
类型名(TypeScript):
OrderStatusUserRole
统一命名的价值:
- 新同事一看就知道“这个是常量对象”“这个是文案字典”。
- 代码搜索体验更好,比如搜 “
_STATUS” 一般都能搜到状态相关的定义。
8.2 通用模板文件(建议直接放进项目)
可以在项目里建一个 src/constants/_template.ts,作为新模块的起点。
// src/constants/_template.ts
/**
* 示例:某业务模块的常量模板
* 每次有新模块(例如:工单、任务、审批),可以复制本文件改名使用。
*/
// 1. 枚举值:给接口 / 数据库用
export const MODULE_STATUS = {
TODO: 0,
DOING: 1,
DONE: 2,
} as const;
// 2. TypeScript 类型(如使用 TS)
export type ModuleStatus = (typeof MODULE_STATUS)[keyof typeof MODULE_STATUS];
// 3. 展示文案字典(码值 → 文案)
export const MODULE_STATUS_LABEL = {
[MODULE_STATUS.TODO]: '待处理',
[MODULE_STATUS.DOING]: '进行中',
[MODULE_STATUS.DONE]: '已完成',
} as const;
// 4. 样式 / 颜色字典(码值 → UI)
export const MODULE_STATUS_COLOR = {
[MODULE_STATUS.TODO]: 'info',
[MODULE_STATUS.DOING]: 'warning',
[MODULE_STATUS.DONE]: 'success',
} as const;
// 5. 下拉 options(用于表单 / 筛选)
export const MODULE_STATUS_OPTIONS = Object.entries(MODULE_STATUS).map(([_, value]) => ({
label: MODULE_STATUS_LABEL[value as number],
value,
}));
团队落地建议:
- 当有人要新建一个“状态枚举”时,先复制这个模板。
- 保持所有模块的写法一致,新人也能一眼看懂。
九、如何把这套内容变成你自己团队的规范?
如果你是 TL 或老前端,完全可以直接基于这篇文章,整理一份团队规范:
-
规范标题建议
- 《XXX 项目前端常量管理规范》
- 《前端状态枚举与字典统一规范》
-
目录结构可以照抄本文:
- 背锅代码引入(先展示“魔法数字”的危害)
- 基本概念:魔法数字 / 魔法字符串 & 为何要禁止
- Vue 项目中的常量目录结构(附一张目录截图更好)
- 实战案例:订单模块常量 + Vue 使用示例
- 进阶用法:JS / TS 枚举写法、i18n 支持
- 踩坑总结:前后端不一致、多份定义、写在组件内部
- 落地模板:给出
_template.ts模板文件
-
执行方式建议
- 先在一个功能模块里试点(例如订单、用户)。
- 在 Code Review 中强制要求:业务代码中不得直接出现“魔法数字 / 魔法字符串”(业务含义相关的)。
- 渐进式重构旧代码,不需要一次性全推倒。
十、总结:常量管理不是“洁癖”,是工程化的起点
最后用几句话收个尾:
- 常量管理的本质,是给“业务含义”起一个统一的、可搜索、可维护的名字。
- 把零散的数字和字符串收敛到一处,是工程化和团队协作的基础。
- 对个人来说,这是一个“经验型前端”向“工程型前端”升级的非常关键的一步。
如果你已经有一个真实项目,建议从一个最常见的模块(比如“订单状态”“用户角色”)开始,
把里面的魔法数字 / 魔法字符串全部替换成规范的常量和字典,这本身就可以写成你下一篇的实战文章。
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~