VUE2工程中字典的统一处理

344 阅读4分钟

背景:在我们的项目中用到了大量的字典数据,如下拉框,表格的字段映射..。而这些字典数据的来源不一,有的来自于数据库参数,有的来自于字典表,有的配置在前端。这其中有几个问题,让我觉得处理的不是特别优雅

  1. 每次从后端查询的字典数据均需要二次封装,比如:

后端返回的数据结构是:

{
code: "ABC",
desc: "xxxxx"
}

而我们想要的字典格式是:

{
code: "ABC",
desc: "ABC - xxxxx"
}
  1. 字典数据来源很多,且每种字典的获取的处理方式不同:

通过数据库参数获取的字典:

// 通过参数获取字典的接口,每个接口只能获取一个参数表
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;
    },
  1. 获取字典后对于表格的字段映射,通常需要创建一个计算属性:
<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
      }
    }
  },
  1. 对于从后端获取的字典数据,每个页面都需要重新请求,而这些数据通常是不容易变化的

于是我就想能不能将所用的字典进行统一处理:

  1. 针对不同的字典获取方式,提供统一的配置,通过读取配置获取
// 比如通过读取vue配置项中的dict对象
export default {
	dict: {
		param: [
			{
				id: 'xxx',
				code: 'parm_xxx$desc$codeId'
			}
		],
		sysDict: [
			{
				id: 'yyy',
				code: 'YYY'
			}
		],
		local: [
			{
				id: 'zzz',
				code: 'zzz'
			}
		]
	}
}
  1. 获取字典数据后处理为统一的格式返回
  1. 提供生成计算属性选项,对需要生成计算属性的字典返回处理后的字典数据的同时,返回对应的计算属性
export default {
	dict: {
		param: [
			{
				id: 'xxx', 
				code: 'parm_xxx$desc$codeId',
				computed: true
			}
		],
		sysDict: [
			{
				id: 'yyy',
				code: 'YYY'
			}
		],
		local: [
			{
				id: 'zzz',
				code: 'zzz'
			}
		]
	}
}
  1. 将已经获取过的字典数据缓存,下次访问相同的字典数据时,直接从缓存获取,计算属性不进入缓存,每次返回字典数据的时候,再生成
  1. 暴露清空缓存的方法,在需要时使用

具体实现:

我决定使用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)
	}
}