深入挖掘前端基础服务&中间件设计-字典设计

1,183 阅读6分钟

前言

字典设计,可能有的小伙伴还不清楚,字典是什么?项目里面的字典又是什么?

后端的同学可能感触比较多,类似于枚举一样,对系统内的可以穷举出来的字典属性,而且通用性较强。一般会放到字典中进行维护。

如:性别 sex 男/女/其他

一般中小型项目,一般不会去做字典的管理,因为这类项目的讲求就是快、稳定

项目稍微大一些的时候,具备一定的规模

  • 庞大的需求、
  • 人员规模增加,
  • 多人协作效率要求高

这个时候就需要考虑项目里对这类数据进行管理,我们也叫字典管理

如果不这么做,也是可以的,但是会搞的比较麻烦

如不同的人,在开发对应功能模块的时候,可能就为了获取一个类型的数据集合,需要让后端专门去开个接口,

进行独立的接口请求,返回响应的类型数据集合,返回后在做数据的转换,供业务逻辑或UI组件使用。

想想吧,多劝劝框架设计和项目经理吧。。。

我们来进行字典管理的设计

设计思路

image.png

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中,内部支持数据字典转换

image.png

/**
*  注册字典
* @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对象提供的能力,对于上层应用提供了那些服务。

字段管理器能力设计:

image.png

从上图可以看出,管理器向外提供的能力,从不同的维度进行思考。
实现向内封闭
使用方式必须简单易懂
具备向外扩展的能力 只对数据负责,耦合性必须低

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、字典初始化-注册,提供了registerregisterTree,对扁平数据集合List和扁平数据Tree都可以加载,加载进来后,对数据进行遍历,字典进行映射转换,处理完数据后,

3、存储在对象中dictMap中,内部的数据存储在immer的produce下,produce方法里面会通过createProxy方法创建一个代理对象。

主要目的是对数据进行保护,克隆数据对象,只提供提取的能力。

以前的第一个版本使用比较简单,就一个 data: produce({}, () => {}),对象,提供updateDictMap更新能力。

后来需要对外提供扩展能力,进行了改造,改造后,提供了 sourceData数据集合,

就是说,在字典注册后,会把注册的数据,放入到原数据缓存对象中

真实使用时,会先从 EDUDict.data[typeCode]先从缓存的对象中找,这种情况就是:

  • 如果已经使用过一次,第二次使用时,会直接查找出来返回

  • 如果没有加载使用过,会EDUDict.sourceData去查找,找到后,EDUDict.updateDictMap 会放入到data

image.png

老铁们,我们一起关注系列设计:

深入挖掘前端基础服务&中间件设计-basic-library