前端常量管理规范:拒绝魔法数字,统一枚举与字典|项目规范篇

4 阅读12分钟

【前端常量管理 + Vue/TS 项目】:从核心规范到落地实操,彻底搞懂业务常量/枚举/字典的最佳写法,避开前后端状态不一致、多份定义、维护难高频坑!

在这里插入图片描述

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。

一、先看一段“背锅代码”

先从一个真实到不能再真实的例子开始:


// 某个订单列表页
if (order.status === 1) {
  showPayButton = true;
}

if (order.status === 3) {
  showCancelButton = false;
}

刚写完的时候你可能记得:

  • 1 代表“待支付”
  • 3 代表“已取消”

但是问题来了:

  • 过了两周,你还记得吗?
  • 新同事看这段代码,会知道 13 是啥吗?
  • 产品突然说:“未支付状态从 1 改成 10”——你准备怎么改?

这段代码至少有三个问题:

  • 可读性差13 完全没有语义,靠“记忆”来理解。
  • 难维护:要改状态值,只能全局搜索 13,非常不安全。
  • 易出 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;
}

这样就立刻好很多:

  • 读得懂:一眼就知道 UNPAIDCANCELED 是什么状态。
  • 好维护:状态码改了,只改一个地方。
  • 好搜索:全局搜 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 订单
}

存在的问题:

  • 看不懂12VIP_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.tsorder.tsproduct.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_STATUS
    • USER_ROLE
    • YES_NO
  • 字典对象(基于枚举再衍生的映射):在后面加功能后缀

    • ORDER_STATUS_LABEL
    • ORDER_STATUS_COLOR
    • USER_ROLE_OPTIONS
  • 类型名(TypeScript)

    • OrderStatus
    • UserRole

统一命名的价值:

  • 新同事一看就知道“这个是常量对象”“这个是文案字典”。
  • 代码搜索体验更好,比如搜 “_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 项目前端常量管理规范》
    • 《前端状态枚举与字典统一规范》
  • 目录结构可以照抄本文:

    1. 背锅代码引入(先展示“魔法数字”的危害)
    2. 基本概念:魔法数字 / 魔法字符串 & 为何要禁止
    3. Vue 项目中的常量目录结构(附一张目录截图更好)
    4. 实战案例:订单模块常量 + Vue 使用示例
    5. 进阶用法:JS / TS 枚举写法、i18n 支持
    6. 踩坑总结:前后端不一致、多份定义、写在组件内部
    7. 落地模板:给出 _template.ts 模板文件
  • 执行方式建议

    • 先在一个功能模块里试点(例如订单、用户)。
    • 在 Code Review 中强制要求:业务代码中不得直接出现“魔法数字 / 魔法字符串”(业务含义相关的)。
    • 渐进式重构旧代码,不需要一次性全推倒。

十、总结:常量管理不是“洁癖”,是工程化的起点

最后用几句话收个尾:

  • 常量管理的本质,是给“业务含义”起一个统一的、可搜索、可维护的名字。
  • 把零散的数字和字符串收敛到一处,是工程化和团队协作的基础。
  • 对个人来说,这是一个“经验型前端”向“工程型前端”升级的非常关键的一步。

如果你已经有一个真实项目,建议从一个最常见的模块(比如“订单状态”“用户角色”)开始,

把里面的魔法数字 / 魔法字符串全部替换成规范的常量和字典,这本身就可以写成你下一篇的实战文章。


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~