愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

关于前端枚举的一点思考

一、枚举的业务场景与遇到的问题

我们在日常业务开发中,经常遇到枚举,如商品状态、页面状态、审核状态等,翻阅以往的一些业务代码,会发现很多地方都是这么写的:

<span v-if="status == 0">审核中</span>
<span v-else-if="status == 1">审核通过</span>

以上代码其实存在以下问题:

  • 魔数化:一旦有个数值改动,就得全局替换匹配
  • 差语义化:无法直观通过值推导出含义

于是,进阶做法我们想的是通过引入常量,如:

<span v-if="status == STATUS.AUDITING">审核中</span>
<span v-else-if="status == STATUS.PASS">审核通过</span>

然而,我们其实还会遇到以下场景:

  • 需要拿到枚举值获得枚举含义

特别是在列表中尤其常见。我们通常的做法是:建立过滤器,建立Map,如:

export default {
    // ...
    filters: {
        statusName: status => {
            const map = {
                [STATUS.AUDITING]: '审核中',
                [STATUS.PASS]: '审核通过'
            }
            return map[status] || ''
        }
    }
}

这样子虽然也挺方便,但是仍然不够完美:

  • 定义隔离:枚举值和枚举含义分离,还是会带来一定维护上的问题


二、如何改善?

我们期望的应该是定义能够一体化,而不是分散化。参考Java里的枚举做法,其实好很多:

public enum AgingTypeEnum {
    ALL(0, "全部"),
    SPECIAL(1, "特批时效"),
    PLATFORM(2, "平台定义");
    // ...
}

但是,虽然TS里也实现了enum,但其实做法还是有些不一样,还是不那么利于我们的业务场景。因为TS里的enum,也不是枚举值与含义定义一体化。对于我们的业务场景,可能下面这么做更利于我们的使用:

const STATUS = createEnum({
    AUDITING: [0, '审核中'],
    PASS: [1, '审核通过']
})

export default {
    // ...
    created() {
        this.STATUS = STATUS
    }
}
<span v-if="status == STATUS.AUDITING">审核中</span>
<span v-else-if="status == STATUS.PASS">审核通过</span>

<p>当前状态:{STATUS.getDescFromValue(syncData.status)}</p>
<p>也可用通过枚举名称获取描述:{STATUS.getDesc('AUDITING')}</p>

如此一来,具有以下好处:

  • 去魔数化:后端假如改了审核中状态为2,那么我们就只需要在开头把0改为2即可
  • 语义化:通过STATUS.AUDITING我们就可以大概猜出含义
  • 定义一体化:枚举值和枚举描述写在了一起,不分散
  • 使用方便:无需额外的过滤器,就可以通过枚举名称/枚举值获得枚举含义

三、实现

实现代码如下:

/**
 * 枚举定义工具
 * 示例:
 * const STATUS = createEnum({
 *     AUDIT_WAIT: [1, '审核中'],
 *     AUDIT_PASS: [2, '审核通过']
 * })
 * 获取枚举值:STATUS.AUDIT_WAIT
 * 获取枚举描述:STATUS.getDesc('AUDIT_WAIT')
 * 通过枚举值获取描述:STATUS.getDescFromValue(STATUS.WAIT)
 * 
 * Created by hzliurufei on 2018-12-17 21:23:28 
 * @Last Modified by: hzliurufei
 * @Last Modified time: 2018-12-17 21:47:15
 */

interface EnumDefinition {
    [enumName: string]: [number, string]
}

export default function createEnum(definition: EnumDefinition) {
    const strToValueMap = {}
    const numToDescMap = {}
    for (const enumName of Object.keys(definition)) {
        const [value, desc]: [number, string] = definition[enumName]
        strToValueMap[enumName] = value
        numToDescMap[value] = desc
    }
    return {
        ...strToValueMap,
        getDesc(enumName: string): string {
            return definition[enumName] && definition[enumName][1] || ''
        },
        getDescFromValue(value: number): string {
            return numToDescMap[value] || ''
        }
    }
}

非TS版:

export default function createEnum(definition) {
    const strToValueMap = {}
    const numToDescMap = {}
    for (const enumName of Object.keys(definition)) {
        const [value, desc] = definition[enumName]
        strToValueMap[enumName] = value
        numToDescMap[value] = desc
    }
    return {
        ...strToValueMap,
        getDesc(enumName) {
            return definition[enumName] && definition[enumName][1] || ''
        },
        getDescFromValue(value) {
            return numToDescMap[value] || ''
        }
    }
}