前言
字典设计,可能有的小伙伴还不清楚,字典是什么?项目里面的字典又是什么?
后端的同学可能感触比较多,类似于
枚举
一样,对系统内的可以穷举出来的字典属性,而且通用性较强。一般会放到字典中进行维护。如:
性别 sex
男/女/其他
一般中小型项目,一般不会去做字典的管理,因为这类项目的讲求就是快、稳定
。
项目稍微大一些的时候,具备一定的规模
- 庞大的需求、
- 人员规模增加,
- 多人协作效率要求高
这个时候就需要考虑项目里对这类数据进行管理,我们也叫字典管理
。
如果不这么做
,也是可以的,但是会搞的比较麻烦
如不同的人,在开发对应功能模块的时候,可能就为了获取一个类型的数据集合,需要让后端专门去开个接口,
进行独立的接口请求,返回响应的类型数据集合,返回后在做数据的转换,供业务逻辑或UI组件使用。
想想吧,多劝劝框架设计和项目经理吧。。。
我们来进行字典管理的设计
设计思路
step1:
用户登录后,服务需要设计一个接口,,约定好返回系统所有的字典信息,可以是扁平化的数据List集合或者树形数据集合
step2:
获取到字典数据后,调用框架层基础服务的 basic-library
Dict对象下的register方法,将字典集合数据注册到全局字段管理器中。
step3:
业务开发人员,功能开发时,如果遇到需要使用字典的地方,调用基础服务的 basic-library
的Dict对象下的
useDict
, 传入 字典Code(typeCode ),即可以获取当前字典下的数据集合,供UI组件或者业务逻辑使用。
业务逻辑使用:
import { Dict } from '@basic-library'
Dict.useDict('字典的typeCode')
* typeCode = 16500 返回结果:
[
{ value: "0", label: '未发布'},
{ value: "1", label: '已发布'},
{ value: "2", label: '部分发布'},
]
// 根据 typecode和value获取显示值
const label = Dict.getLabel('16400','1')
// 根据 typecode和label获取实际值
const value = Dict.getValue('16400','女')
基础组件字典方式:
// 下拉框 enums 为字典的typeCode
<edu-select enums='16500'></edu-select>
// 单选框 enums 为字典的typeCode
<edu-radio enums='16500'></edu-select>
// 复选框 enums 为字典的typeCode
<edu-checkbox enums='16500'></edu-select>
组件层面,不是天然的UI组件可以支持的,可以对基础组件、复合组件、业务组件做二次封装或者扩展,来对接字典。
字典注册
系统运行时,对字典集合数据注册到框架层的字典管理器中,字典管理器进行数据转换和存储,向外提供获取能力
目前设计上,支持前端开发自定义字典注册和远程字典注册。
自定义字典:
也叫本地静态字典,就是在本地公共目录下,使用静态文件.js
或者.json
静态导入方式或者异步请求加载,进行配置
src\common\config\typeCodes.js
const typeCodes = [
{ code: "1", name: '男', enumType: '16400' },
{ code: "2", name: '女', enumType: '16400' },
{ code: "0", name: '未发布', enumType: '16500' },
{ code: "1", name: '已发布', enumType: '16500' },
{ code: "2", name: '部分发布', enumType: '16500' },
]
export default typeCodes
使用分析
这种方式,对于一些临时字典、简单的字典、需求上模糊不清无法决策的,根据实际情况,前端可以自定义部分。
如:字典Code为 16400 性别
{ code: "1", name: '男', typecode: '16400'},
{ code: "2", name: '女', typecode: '16400'},
Dict.useDict('16400')
,实际使用中与远程请求字典是的方式是一致的。
远程字典注册:
系统加载时,请求服务端全局字典接口,queryAllDictList,然后将返回的数据集合,注册到Dict.register
中,内部支持数据字典转换
/**
* 注册字典
* @param {*} No
* 场景: 系统加载
*/
const registerDict = async () => {
Dict.clearAll()
// 字典注册 -- 静态
Dict.register(typeCodes)
// 字典注册 -- 远程
const { loginStatus } = BaseStore.app
if (CONFIG?.DICT_REMOTES && loginStatus) {
const { returnObj = [] } = await Service.useHttp('queryAllDictList');
returnObj?.length && Dict.register(returnObj, { name: 'name', code: 'code', type: 'dictType' })
}
}
Dict.useDict('classType')
,实际使用方式保持一致。
下来,我们看看,这个框架层Dict对象提供的能力,对于上层应用提供了那些服务。
字段管理器能力设计:
从上图可以看出,管理器向外提供的能力,从不同的维度进行思考。
实现向内封闭
使用方式必须简单易懂
具备向外扩展的能力
只对数据负责,耦合性必须低
API
参数 | 说明 |
---|---|
useDict | 根据字典code获取字典集合 |
useDicts | 获取所有的字典集合 |
getLabel | 获取单个字段的显示名称-label |
getValue | 获取单个字段的实际值-value |
format | 字典字段转换 label/value 映射 |
push | 扩展类:插入或者覆盖字典对象 |
getExt | 扩展方法,根据字段条件,获取字典记录 |
register | 静态字典注册 |
实践代码:
/**
* 字典管理器
*/
import { treeHelper } from '@basic-utils';
import { formatItemTextExt } from './utils';
import EDUDict from './cache';
/**
* 获取单个字段集合
* @param {*} typeCode
* @returns
*/
function use(typeCode) {
return useDict(typeCode)
}
/**
* 获取单个字段集合
* @param {*} typeCode
* @returns
*/
function useDict(typeCode) {
const codes = EDUDict.data[typeCode]
if (typeCode && !codes) {
const dicList = EDUDict.sourceData.filter((record) => record.enumType == typeCode)
if (!dicList.length) {
console.warn(`字典${typeCode}获取失败!请使用window._EDU_DICT_查看`)
} else {
EDUDict.updateDictMap(typeCode, dicList);
}
}
return EDUDict.data[typeCode] || [];
}
/**
* 字典语法糖
* 获取单个字段的显示名称-label
* @param {*} typeCode 字典编码
* @param {*} value 字典值
* @returns
*/
function getLabel(typeCode, value) {
return this.useDict(typeCode).find((item) => item.value === value)?.label;
}
/**
* 字典语法糖
* 获取单个字段的实际值-value
* @param {*} typeCode 字典编码
* @param {*} label 字典显示名称
* @returns
*/
function getValue(typeCode, label) {
return this.useDict(typeCode).find((item) => item.label === label)?.value;
}
/**
* 字典语法糖--条件策略
* 获取单个字段的值-{fieldKey:fieldValue}-获取当前字典行对象
* @param {*} typeCode 字典编码
* @param {*} object1 对象 {fieldKey: fieldValue}
* @returns
*/
function getExt(typeCode, object1) {
for (const [key, value] of Object.entries(object1)) {
return this.useDict(typeCode).find((item) => item[key] === value) || {};
}
}
/**
* 获取所有的字典集合
* @returns
*/
function useDicts() {
return EDUDict.data;
}
/**
* 字典初始化--树形结构数据
* @param {*} dicts
*/
function registerTree(dicts) {
// 对数据进行转换--扁平数据==> 树状结构
const data = treeHelper.computTreeList(dicts || [], 'code', 'parentCode');
data.forEach((item) => {
const typeCode = item.code
item.children?.forEach((subItem) => {
subItem.label = subItem.name;
subItem.value = subItem.code;
subItem = { ...subItem, ...formatItemTextExt(subItem.extText) };
});
EDUDict.updateDictMap(typeCode, item.children);
});
}
/**
* 字典初始化
* @param {*} dicts
*/
function register(dicts, fd = { name:'name', code :'code', type : 'enumType' }) {
// 对数据进行转换--扁平数据==> 树状结构
const data = dicts || [];
data.forEach((item) => {
item.label = item[fd.name];
item.value = item[fd.code];
item.enumType = item[fd.type];
item = { ...item, ...formatItemTextExt(item.extText) };
EDUDict.insert(item);
});
}
/**
* 字典初始化--外部扩展使用
* @param {*} typeCode 字典标识
* @param {*} compound 字典集合arraylist |获取字典 Promise.resolve()返回对象
* @param {*} cover 是否覆盖标识 默认为false
* Dict.push('字典类型',async()=> (await Service.useHttp("getDictionaryList",'type=activityType')).returnObj,
{label: "dictionaryName",value: "dictionaryCode"}).then(res=>{console.log('字典类型 :>> ', res);})
*/
async function push(typeCode, compound, option={}) {
const codes = EDUDict.data[typeCode]
let results = []
if (typeCode && !codes) {
if(Array.isArray(dic)){
results = compound
}else{
try {
results = format(await compound(),option);
} catch (error) {
console.error(`字典${typeCode}加载失败!`,error)
}
}
results.length && EDUDict.updateDictMap(typeCode, results);
}
return EDUDict.data[typeCode] || [];
}
/**
* @description: 字典字段转换
* @param {*} list 来源字典列表 Array
* @param {*} label 转换 label 对象
* @param {*} value 转换 value 对象
* @return {*} 转换后list
* @author: daerduo
*/
function format(list=[], {label='label' , value='value'}){
return list?.map((item)=>{
item.label = item[label];
item.value = item[value];
return item
})
}
/**
* @description: 清空字典
* @return {*}
* @author: daerduo
*/
function clearAll(){
EDUDict.clear()
}
const Dict = {
register,
use,
useDict,
useDicts,
getLabel,
getValue,
format,
push,
getExt,
clearAll,
};
export default Dict;
cache.js
/*
* @Description: 字典集合存储
* @Author: daerduo
*/
import produce from 'immer';
let dictMap = {
mode: 'multi',
data: produce({}, () => {}),
sourceData: produce([], () => {}),
updateDictMap(key, value) {
if(this.mode == 'multi'){
this.data[key] = value
}else{
this.data = produce(this.data, (draft) => {
draft[key] = value;
});
}
},
insert(value) {
this.sourceData = produce(this.sourceData, (draft) => {
draft.push(value)
})
},
clear() {
this.data = [],
this.sourceData = []
},
};
const EDUDict = (function () {
if (window._EDU_DICT_) {
return window._EDU_DICT_;
} else {
window._EDU_DICT_ = dictMap;
return dictMap;
}
})();
export default EDUDict;
utils.js
export function formatItemTextExt(text) {
let ext = {};
if (!text) {
return ext;
}
try {
ext = JSON.stringify(text);
} catch (e) {}
return ext;
}
核心源码解读:
1、字典的基本方法实现都比较简单,比如获取数据类、转换数据类,以及更高级的语法糖。这些都建立在字典的存储和注册上
2、字典初始化-注册,提供了register
和registerTree
,对扁平数据集合List和扁平数据Tree都可以加载,加载进来后,对数据进行遍历,字典进行映射转换,处理完数据后,
3、存储在对象中dictMap中,内部的数据存储在immer的produce下,produce方法里面会通过createProxy方法创建一个代理对象。,
主要目的是对数据进行保护,克隆数据对象,只提供提取的能力。
以前的第一个版本使用比较简单,就一个
data: produce({}, () => {}),
对象,提供updateDictMap
更新能力。后来需要对外提供扩展能力,进行了改造,改造后,提供了
sourceData
数据集合,
就是说,在字典注册后,会把注册的数据,放入到原数据缓存对象中
真实使用时,会先从 EDUDict.data[typeCode]
先从缓存的对象中找,这种情况就是:
-
如果已经使用过一次,第二次使用时,会直接查找出来返回
-
如果没有加载使用过,会
EDUDict.sourceData
去查找,找到后,EDUDict.updateDictMap
会放入到data
中
老铁们,我们一起关注系列设计: