背景:在我们的项目中用到了大量的字典数据,如下拉框,表格的字段映射..。而这些字典数据的来源不一,有的来自于数据库参数,有的来自于字典表,有的配置在前端。这其中有几个问题,让我觉得处理的不是特别优雅
- 每次从后端查询的字典数据均需要二次封装,比如:
后端返回的数据结构是:
{
code: "ABC",
desc: "xxxxx"
}
而我们想要的字典格式是:
{
code: "ABC",
desc: "ABC - xxxxx"
}
- 字典数据来源很多,且每种字典的获取的处理方式不同:
通过数据库参数获取的字典:
// 通过参数获取字典的接口,每个接口只能获取一个参数表
async getParamSelect() {
const param = {
tableNameAndTypeIdAndTypeName: 'parm_xxx$desc$codeId'
}
const {data} = await getCommonSelectList(param);
this.selectXXXList = data.map(item => {
return {
value: item.codeId,
label: item.codeId + '-' + item.desc
}
});
}
通过字典表获取的字典:
// 通过字典表获取字典的接口,可以同时获取多个字典
async getDictSelect() {
const param = {
typeIds: "xxx,yyy",
};
const { data } = await getDictSelectList(param);
this.selectXXXList = data[xxx].map((item) => {
return {
value: item.codeId,
label: item.codeId + "-" + item.desc,
};
})
this.selectYYYList = data[yyy].map((item) => {
return {
value: item.codeId,
label: item.codeId + "-" + item.desc,
};
})
}
获取前端本地的字典:
// 本地字典已注册到全局
getLocalSelect() {
this.selectXXXList = this.$localDict.xxx;
},
- 获取字典后对于表格的字段映射,通常需要创建一个计算属性:
<a-table :dataSource="data">
<template slot="XXX", slot-scope="record">
{{ XXXConvert(record.xxx) }}
</template>
</a-table>
computed: {
XXXConvert() {
return function(val) {
if(!val) {
return ''
}
let xxx = ''
this.selectXXXList.forEach(item => {
if(item.value == val) {
xxx = item.label
}
})
return xxx
}
}
},
- 对于从后端获取的字典数据,每个页面都需要重新请求,而这些数据通常是不容易变化的
于是我就想能不能将所用的字典进行统一处理:
- 针对不同的字典获取方式,提供统一的配置,通过读取配置获取
// 比如通过读取vue配置项中的dict对象
export default {
dict: {
param: [
{
id: 'xxx',
code: 'parm_xxx$desc$codeId'
}
],
sysDict: [
{
id: 'yyy',
code: 'YYY'
}
],
local: [
{
id: 'zzz',
code: 'zzz'
}
]
}
}
- 获取字典数据后处理为统一的格式返回
- 提供生成计算属性选项,对需要生成计算属性的字典返回处理后的字典数据的同时,返回对应的计算属性
export default {
dict: {
param: [
{
id: 'xxx',
code: 'parm_xxx$desc$codeId',
computed: true
}
],
sysDict: [
{
id: 'yyy',
code: 'YYY'
}
],
local: [
{
id: 'zzz',
code: 'zzz'
}
]
}
}
- 将已经获取过的字典数据缓存,下次访问相同的字典数据时,直接从缓存获取,计算属性不进入缓存,每次返回字典数据的时候,再生成
- 暴露清空缓存的方法,在需要时使用
具体实现:
我决定使用vue插件的来实现,插件通过读取dict配置,然后通过mixin往实例上增加selectXXXList和XXXConvert
dict插件:
export default {
install(Vue) {
Vue.mixin({
data() {
return {
dictData: {},
};
},
async mounted() {
const { dict } = this.$options;
if (!dict) {
return;
}
const { param, sysDict, local } = dict;
if (param && param.length > 0) {
// 获取参数字典
for (let p of param) {
const result = await fetchDictData("param", p.code);
this.$set(this.dictData, p.id, result);
}
}
if (sysDict && sysDict.length > 0) {
// 获取系统字典
const revert = Object.fromEntries(
Object.entries(sysDict).map(([key, value]) => [value, key])
);
const dictArr = sysDict.map((item) => item.code);
const result = await fetchDictData("sysDict", dictArr);
for (let key in result) {
this.$set(this.dictData, revert[key], result[key]);
}
}
if (local && local.length > 0) {
// 获取本地字典
for (let l of local) {
this.$set(this.dictData, l.id, this.$localDict[l.code]);
}
}
},
});
},
};
const fetchDictData = async (type, code) => {
let result = [];
if (type === "param") {
const params = {
tableNameAndTypeIdAndTypeName: code,
};
const { data } = await getCommonSelectList(params);
result = data.map((item) => ({
value: item.codeId,
label: `${item.codeId} - ${item.desc}`,
}));
} else if (type === "sysDict") {
const param = {
typeIds: code,
};
const { data } = await getDictSelectList(param);
for (let key in data) {
result[key] = data[key].map((item) => ({
value: item.codeId,
label: `${item.codeId} - ${item.desc}`,
}));
}
}
return result;
};
这样一来,在vue组件中就可以通过this.dictData获取具体的字典,比如:
export default {
dict: {
param: [
{
id: 'xxx',
code: 'parm_xxx$desc$codeId'
}
],
sysDict: [
{
id: 'yyy',
code: 'YYY'
}
],
local: [
{
id: 'zzz',
code: 'zzz'
}
]
}
methods:{
getXXXSelectList() {
return this.dictData.xxx
}
}
}
对于自动添加computed,我没有想到好的处理方式,只有采用一种折中的处理:
// 在混合中提供一个生产computed的方法
Vue.mixin({
data() {
return {
dictData: {},
};
},
methods: {
computedFactory(dictId) {
return val => {
const dict = this.dictData[dictId] || [];
const item = dict.find(item => item.value === val);
return item ? item.label : '';
};
},
},
});
在vue组件中使用:
export default {
dict: {
param: [
{
id: 'xxx',
code: 'parm_xxx$desc$codeId'
}
],
sysDict: [
{
id: 'yyy',
code: 'YYY'
}
],
local: [
{
id: 'zzz',
code: 'zzz'
}
]
},
computed: {
XXXConvert() {
return this.computedFactory('xxx');
},
},
}
这样一来,也不用配置是否生成计算属性选项,只有在调用computedFactory时才会生成
最后对于字典的缓存,我觉得应该使用字典的code作为标识,因为每个vue页面的编写者对于字典id的定义不一定是一样的,但是code一定是相同的(code才是表里存的数据)
另外还有一个问题,字典表的查询是多个字典同时查询,所以我需要先把需要查询的字典分离出来,看哪些已经被缓存过了,把没有缓存过的字典重新拼接起来,发起查询。最后将缓存和查询结果再拼接起来返回
所以需要改造fetchDictData方法
const fetchDictData = async (type, code) => {
let result = [];
if (type === "param") {
if (cache[code]) {
return cache[code]; // 参数字典的缓存直接返回
}
const params = {
tableNameAndTypeIdAndTypeName: code,
};
const { data } = await getCommonSelectList(params);
result = data.map((item) => ({
value: item.codeId,
label: `${item.codeId} - ${item.desc}`,
}));
cache[code] = result;
} else if (type === "sysDict") {
// 字典表的,先拆分出已缓存的和未缓存的,然后分别获取
const codeArr = code.split(",");
const notCache = codeArr.filter((item) => !cache[item]);
const hasCache = codeArr.filter((item) => cache[item]);
if (hasCache.length > 0) {
result = hasCache.reduce((acc, cur) => {
acc[cur] = cache[cur];
return acc;
}, {});
}
if (notCache.length > 0) {
const param = {
typeIds: notCache.join(","),
};
const { data } = await getDictSelectList(param);
for (let key in data) {
result[key] = data[key].map((item) => ({
value: item.codeId,
label: `${item.codeId} - ${item.desc}`,
}));
cache[key] = result[key];
}
}
}
return result;
};
最后看完整代码
Dict插件
import { getCommonSelectList, getDictSelectList } from "@/api/common";
export default {
install(Vue) {
const cache = {};
const fetchDictData = async (type, code) => {
let result = [];
if (type === "param") {
if (cache[code]) {
return cache[code]; // 参数字典的缓存直接返回
}
const params = {
tableNameAndTypeIdAndTypeName: code,
};
const { data } = await getCommonSelectList(params);
result = data.map((item) => ({
value: item.codeId,
label: `${item.codeId} - ${item.desc}`,
}));
cache[code] = result;
} else if (type === "sysDict") {
// 字典表的,先拆分出已缓存的和未缓存的,然后分别获取
const codeArr = code.split(",");
const notCache = codeArr.filter((item) => !cache[item]);
const hasCache = codeArr.filter((item) => cache[item]);
if (hasCache.length > 0) {
result = hasCache.reduce((acc, cur) => {
acc[cur] = cache[cur];
return acc;
}, {});
}
if (notCache.length > 0) {
const param = {
typeIds: notCache.join(","),
};
const { data } = await getDictSelectList(param);
for (let key in data) {
result[key] = data[key].map((item) => ({
value: item.codeId,
label: `${item.codeId} - ${item.desc}`,
}));
cache[key] = result[key];
}
}
}
return result;
};
Vue.mixin({
data() {
return {
dictData: {},
};
},
async mounted() {
const { dict } = this.$options;
if (!dict) {
return;
}
const { param, sysDict, local } = dict;
if (param && param.length > 0) {
// 获取参数字典
for (let p of param) {
const result = await fetchDictData("param", p.code);
this.$set(this.dictData, p.id, result);
}
}
if (sysDict && sysDict.length > 0) {
// 获取系统字典
const revert = Object.fromEntries(
Object.entries(sysDict).map(([key, value]) => [value, key])
);
const dictArr = sysDict.map((item) => item.code).join(",");
const result = await fetchDictData("sysDict", dictArr);
for (let key in result) {
this.$set(this.dictData, revert[key], result[key]);
}
}
if (local && local.length > 0) {
// 获取本地字典
for (let l of local) {
this.$set(this.dictData, l.id, this.$localDict[l.code]);
}
}
},
methods: {
computedFactory(dictId) {
return (val) => {
const dict = this.dictData[dictId] || [];
const item = dict.find((item) => item.value === val);
return item ? item.label : "";
};
},
},
});
// 暴露清空缓存的方法
Vue.prototype.$clearDictCache = () => {
Object.keys(cache).forEach(key => {
delete cache[key];
});
};
},
};
main.js注册
import Vue from 'vue';
import Dict from './plugins/dict';
Vue.use(Dict);
组件中使用:
<a-table :dataSource="data">
<template slot="xxx" slot-scope="record">
{{ XXXConvert(record.xxx) }}
</template>
</a-table>
export default {
dict: {
param: [
{
id: 'xxx',
code: 'parm_xxx$desc$codeId'
}
],
sysDict: [
{
id: 'yyy',
code: 'YYY'
}
],
local: [
{
id: 'zzz',
code: 'zzz'
}
]
},
computed: {
XXXConvert() {
return this.computedFactory('xxx');
},
},
};
其实对于普通项目来说,到这一步就差不多了,但是我们的项目是一个后台管理项目,组件都是封装过的,尤其是表单组件,封装后的表单组件实际在使用时是传入配置项,就像这样:
export const formDataList = [
// 表单一
{
formOption: {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
column: 2, // 表单以几列的方式渲染
},
formList: [
{
label: "账号",
prop: "xxx",
placeholder: "",
type: "select",
disabled: true,
rules: [{ required: true, message: "请输入账号" }],
},
// ...
],
},
// 表单二
// ...
];
所以在给对应下拉框赋值时,需要通过
formDataList[0].formList[0].selectList = xxx
来赋值,但是当下拉框很多时,写大量这样的代码实在不够优雅,所以我决定再在插件中提供一个方法,将字典id和prop对应的下拉框直接全部赋值,是否调用取决于开发页面的人
进一步优化:
Vue.mixin({
methods: {
setSelectList(formDataList) {
setTimeout(() => {
// 延迟执行,等待字典数据加载完毕
formDataList.forEach((item) => {
item.formList.forEach((subItem) => {
for (let key in this.dictData) {
if (subItem.prop === key) {
subItem.selectList = this.dictData[key];
}
}
});
});
});
},
},
});
在vue页面中:
export default {
data() {
return {
formDataList
}
}
mounted() {
this.setSelectList(this.formDataList)
}
}