字典组件封装实践思路浅析

2,394 阅读2分钟

前言

我们在业务开发中,大量字典选择场景,于是产生了个想法 —— 能否封装一个通用型的字典选择组件呢。

期望

  • 组件支持select/radio/checkbox,并可扩展不同的UI选择组件;
  • 组件字典数据缓存,避免重复接口请求;
  • 相同字典组件同时加载,请求并发(n个相同类型的字典同时加载)

实现

这里使用Vue实现,基于ElementUI组件库,但不限于此库。有如下结构:

|-- dict
  |-- config.js // 配置组件映射表
  |-- DictNormal.vue // 组件文件
  |-- mixins.js // 通用逻辑可抽离出来,方便后续独立使用

动态组件

我们期望可以支持select/radio/checkbox等交互组件,使用component实现动态组件。分析选择组件应用时的模板结构,模板结构可约定为:

component is="el-select"
	component is="el-option"

动态组件可以通过is来设定,组件列表配置在config.js中:

// config.js 结构示例
export const tagsMap = {
  'select': {
    tag: 'el-select',
    optionTag: 'el-option',
    optAttrs: {
      label: 'value',
      value: 'key',
    },
  },
  'checkbox': {
    tag: 'el-checkbox-group',
    optionTag: 'el-checkbox',
    optAttrs: {
      label: 'key',
      inner: 'value',
    },
  },
}

组件如下:

<template>
  <component
    :is="tagIns.tag"
    v-on="listeners"
    v-bind="$attrs"
    @change="handleChange"
  >
    <component
      :is="tagIns.optionTag"
      v-for="(opt, idx) in dict"
      :key="opt.key + '-' + idx"
      v-bind="genOptAttrs(opt)"
    >
      <slot
        v-if="genOptAttrs(opt).inner"
        :name="`option_${opt.key}_${idx}}}}`"
        v-bind="genOptAttrs(opt)"
      >
        {{ genOptAttrs(opt).inner }}
      </slot>
    </component>
  </component>
</template>
<script>
import Mixins from './mixins';
import { tagsMap } from './config';
export default {
  name: 'DictNormal',
  mixins: [Mixins],
  props: {
    // 组件类别
    tag: {
      type: String,
      default: 'select',
    },
    // 定制选项标签上挂载的属性
    optAttrs: {
      type: [Function, Object],
    },
  },
  computed: {
    tagIns() {
      let hit = tagsMap[this.tag];
      return hit;
    },
  },
  methods: {
    /**
     * ignore
     */
    genOptAttrs(itm) {
      if (typeof this.optAttrs == 'function') {
        return this.optAttrs.call(this, itm) || {};
      }

      let attrs = {};
      let optAttrs = this.tagIns.optAttrs;

      if (typeof this.optAttrs == 'object') {
        optAttrs = this.optAttrs;
      }

      for (let k in optAttrs) {
        attrs[k] = itm[optAttrs[k]];
      }
      return attrs;
    },
  },
};
</script>

字典数据缓存

缓存位置暂只考虑挂在该组件,也可自定挂载位置。

// mixins.js
const cacheDict = {}; // 缓存

export default {
  props: ['dictType'],
  data() {
    return {
      dict: [], // 字典选项数据
    }
  },
  methods: {
    getDict(type){ return cacheDict[type] },
    fetchDictList(type) { /* 接口请求 */ },
    /** 处理响应数据 */
    responseHandler(res) {
      if (res.error === 0) {
        this.commit(this.dictType, res.data);
      }
    },
    /** 设置数据 */
    commit(key, data = []) {
      this.$emit('on-data', data, key);
      cacheDict[key] = data;
      this.dict = [...data];
    },
  }
}

同字典多个并行加载

这里的并行不是严格上的并行,不纠结于此,明白意思即可。

现有dict-type="grade"字典组件,在表单有两个地方用到,如下:

el-form
	el-form-item prop="onlineGrade"
		DictNormal dict-type="grade"
	el-form-item prop="offlineGrade"
		DictNormal dict-type="grade"

模拟一下,页面初始加载:

可以看到,请求多次触发。为解决这个问题,第一次请求发出,需要做promise初始设置,第二次请求的时候,如果promise已经存在,则直接发送。这时候的promise需要映射到字典类型上。关键代码如下:

// mixins.js
import axios from 'axios';

const cachePromise = {};
// 接口模拟方法
const fetchDict = ({type}) => axios.get('/dict', params: { type })

export default {
  props: ['dictType'],
  created() {
    this.initPromise(this.dictType);
  },
  async initPromise(dictType) {
    if (!cachePromise[dictType]) {
      this.__promise = null;
      cachePromise[dictType] = this.fetchDictList(dictType);
    }
    this.__promise = this.__promise || cachePromise[dictType];
    this.responseHandler(await this.__promise);
  },
  /** 拉取字典数据 */
  async fetchDictList(dictType) {
    if (this.__promise && this.cache) {
      return await this.__promise;
    }
    return await fetchDict({ type: dictType });
  },
  /** 处理响应数据 */
  responseHandler(res) {
    if (res.error === 0) {
      this.commit(this.dictType, res.data);
    }
  },
}

总结

大致的思路就是上面所述的:

  • Promise的使用,解决多个相同的请求触发
  • 缓存来减少重复请求
  • component动态选用组件 如果拓展其他字典交互选择组件(比如tag,icon等),可以开发好后内层、外层组建后,引用配置到config中以供使用。更甚者复用mixins,来快速实现缓存、同时请求等字典组件的基础能力。

感谢阅读,有问题欢迎讨论~