前言
前后端开发中有一个实践就是做一套字典系统,为前后端提供字典映射,尤其是后台管理系统中使用最多。比如基于Element UI的若依的字典系统,它的核心是提供这样的数据内容:
[
{
"dictLabel": "男",
"dictValue": "0"
},
{
"dictLabel": "女",
"dictValue": "1"
},
]
那么,若依系统到底是不是最佳实践呢?
我认为不全是。
我认为的最佳实践
一、管理字典的后台系统依然应该有
有人说字典表系统根本没必要,似乎有他的道理,他意思是说,前端从下拉选择了女
,那么传给后端女
,后端把女
存放到数据库。下次返回的时候,返回的也是汉字女
。这种方案核心就是不做字典,直接传值。这个方案有2个问题:
必须明白,后端执行速度的瓶颈就是数据库操作,数据库的一大原则就是能少存东西就少存,能存字母数字就不存汉字,能存1个字就不存2个字。字典表的作用就是牺牲前端的轻便,让后端和数据库更轻便,这个收益是值得的。
防止
dictLabel
变化导致程序出错。比如车辆进小区,从前传的是入
字串,后来觉得不妥,改为传进
,后来又觉得不妥,改为传进门
,后来还是觉得不妥,改为传进场
,于是,如果查询最早期的数据查到的是入
,查询中期的数据查到的是进
和进门
,查询现在数据查到的是进场
,扯淡不?
另外,字典系统还有一个大作用就是约束开发者。既然有字典系统,那么就必须以字典系统为准,只要定死了0
表示男
,那么任何开发者都不要“另辟蹊径”,让1
代表男
。如果没有字典系统,团队则依然需要一个记录手段来统一字典,反而不如直接用字典系统。
二、字典表的dictValue不应当是数字或数字字符串,而应该是有意义常量
原因很简单,至少有2个原因:
- 比如在template中有这么一句:
<div v-if="serviceType === 2">...</div>
请问,这里serviceType
值为2,代表什么?如果这个“服务类型”字典有10多个项,你背的过吗?你背不过。你每次阅读到这句代码,你都要去字典表查一查。
其实程序界已经有一个名词叫“魔术值”,就是指这种突然出现的1、2、3……,就像看戏法一样云里雾里,你根本看不懂它代表什么。
- 假如有一系列状态:“未付款”、“已付款”、“已接单”、“已送出”、“已送达”、“已评价”,他们的编号是从0~5。后来,发觉这一套状态不够,例如想在“已送达”和“已评价”中间插一个“已验货”,这时候它尽管流程上排在“已评价”前,但是编号上只能是6,这就造成了一种开发上的混乱。
解决方案:
我定义字典的时候这样定义,给dictValue
设置有意义的英文或拼音全写或缩写,而且是大写字母,表示是常量:
[
{
"dictLabel": "餐饮",
"dictValue": "CY"
},
{
"dictLabel": "旅游",
"dictValue": "LY"
},
{
"dictLabel": "家政",
"dictValue": "JZ"
},
]
这时候,<div v-if="serviceType === 'CY'">...</div>
稍加思考就知道CY
是指餐饮
,是不是就解决了问题?
再比如,上面提到的“车辆进小区”问题,当描述“进入”这个概念时,写汉字可以有若干种写法,现在我简写为IN
,则任何时候都不会错。同理,表达出小区,我写为OUT
,任何时候也不会错,即便日后真的觉得IN
、OUT
表达的也有歧义,但是因为OUT
并不显示在前端,所以无所谓。
再比如男
、女
,以0
代表男和以1
代表男都已经存在争议,时间久了,我也会怀疑自己的记忆力,到底是0
还是1
代表男
呢?所以,就以M
代表男
,以F
代表女
,永远不会有问题。
再比如最常用的是
、否
,也不要再用1
、0
,应当用Y
、N
。
三、后端提供总接口,并提供较长的协商缓存
若依并没有提供总接口,而是提供了每一个字典表的接口,这种做法根本没必要,甚至就是错误实践。若依的思路是编写每个vue文件都要去思考引入哪些字典表,少一个都不行,而字典表跟业务字段名又往往不统一,比如业务字段名是isExpired
,字典名是yes_no
,这种对应非常费脑子,导致程序员变成了字段调试员。
另外,若依必须先用Promise.all()
请求到所有字典,then
才能get表格数据,否则表格的某些依赖字典的列会有瞬间的空白,这非常蠢。
解决方案:
应当用一个总接口返回所有字典表,不要用每个字典表的接口。
{
"sex": [
{
"dictValue": "M",
"dictLabel": "男"
},
{
"dictValue": "F",
"dictLabel": "女"
}
],
"yes_no": [
{
"dictValue": "N",
"dictLabel": "否"
},
{
"dictValue": "Y",
"dictLabel": "是"
}
],
"serviceType": [
{
"dictValue": "SMFW",
"dictLabel": "上门服务"
},
{
"dictValue": "DDFW",
"dictLabel": "到店服务"
}
],
"orderStatus": [
{
"dictValue": "UNPAID",
"dictLabel": "未付款"
},
{
"dictValue": "PAID",
"dictLabel": "已付款"
},
{
"dictValue": "ORDER_RECEVIED",
"dictLabel": "已接单"
},
]
}
在项目初始化阶段,在beforeEach中尽早ajax这个接口,将所有字典表一股脑返回来。只有字典表get完成,才执行第一个路由导航。
现在有个问题,这个接口数据字节数会比较大,会不会拖累项目加载呢?并不会,因为这个接口内容一般情况下不会有改动,毕竟字典表不是天天变,所以绝大多数时候的请求都会是304
状态码,也就是要求浏览器使用缓存,所以即便数据量比较大也无所谓。
四、后端自身也要缓存总接口数据
现在已经知道,总接口数据是轻易不会有任何变化的,那么:
后端应利用更高效的缓存方式去缓存总接口数据,而不是每次都从数据库去查询。
凡是对字典表的修改都应触发后端重新缓存数据。
五、前端封装统一的格式器方法
依旧拿若依举例,若依的范例代码中,它是ajax每个字典表,并逐个字典表写各自的格式方法,也就是说:
ajax('sex')...
用于请求性别字典表<el-table>
组件里面的formatter
用sexFormatter
函数将'0'
显示为男
,'1'
显示为女
然后后面有个字典比如是教育程度字典,此时又要请求edu
字典表,又要写一个eduFormatter
函数来映射,很繁琐很累很不愉悦对不对?
解决方案:
针对<el-table-column>
组件的formatter
属性写一个公共方法,比如叫dictFormatter
。这个公共方法可以放到@/src/util/dictFormatter.js
里,然后在main.js
全局引入。
<el-table-column
prop="xxx"
:formatter="dictFormatter"
/>
Element UI给格式器函数会默认传4个参数:row, column, cellValue, index
,你可以打印它们看看。其中column.property
就是这个column的prop
,也就是'xxx'
。现在我们缺一个字典表名称,怎么传?错误的方式是:formatter="dictFormatter('sex')"
,'sex'
会覆盖掉默认参数。正确的可以这么写:
<el-table-column
prop="xxx"
dict="sex"
:formatter="(r,c,v,i) => dictFormatter(r,c,v,i,'sex')"
/>
也就是传入高阶函数,它的作用是让函数dictFormatter
执行。dictFormatter
的最后一个参数此时就是字典表的名字'sex'
。然后我们根据'sex'
这个字典名,去总表里找数据,就很轻松了。
dictFormatter的内部代码具体我就不写了,大致是总字典表先定位到sex
属性上,它是个大数组,遍历这个数组看谁的dictValue
等于cellValue
的值,那它的dictLabel
就是你想要的字符串。
另外一,懂解构赋值的同学又不乐意了,这么写也太麻烦了,还有更简单的:
<el-table-column
prop="xxx"
dict="sex"
:formatter="(...rest) => dictFormatter(...rest,'sex')"
/>
哈哈,其实数一数字符数,一样。。。但是优势是出错概率低,不像r,c,v,i
这样容易漏写。
另外二,能不能拿prop
的值也当做字典的值?从而缺省掉最后那个参数?
当然!在一些场合是可以的!如果prop
的值和字典的值恰好是一致的,就比如都是'sex'
,那么直接写成:formatter="dictFormatter"
会非常爽,但是,有些公共字典就不好说了,比如yes_no
字典表,用来表示“是否为儿童”也行,用来表示“是否同意”也行,prop
可能是'isChildren'
或者'isAgreed'
,但字典表名字是通用的'yes_no'
,显然不能直接对应上,必须写略显复杂的:formatter="(...rest) => dictFormatter(...rest,'yes_no')"
。
总结
在一些字典名跟prop重名前提下,可以缺省一个传参,因此可以直接用
:formatter="dictFormatter"
。不重名的话,必须用
:formatter="(...rest) => dictFormatter(...rest,'sex')"
。dictFormatter
的内部代码我不会在本文提供,原理上面已经说过,大致是先判断第五个参数是否存在,并作出相应处理,没写就认为column.property
的值就是字典名。根据传参或者column.property
定位到总表的某属性上,属性值是个大数组,遍历这个数组看谁的dictValue等于cellValue的值,那它的dictLabel就是你想要的字符串。-
别忘了各种出错可能性,比如:
-
cellValue
的值未定义或空怎么办(通常做法是返回空串) -
cellValue
的值不在字典表里怎么办(通常做法也是返回空串,且用console.warn('...')
报个错,这种现象一般是开发测试的时候出现,或黑客捣乱的时候出现,如果后端判断不严谨的话)。
-