【解决方案】如何管理字典效率更高效?

217 阅读5分钟

大纲

  1. 方法1:通过常量和名称对象维护字典项
  2. 方法2:通过选项维护字典项
  3. 方法3:通过配置维护字典,推荐✅

前言

在项目中,经常会遇到多处使用相同列表项的情况,并且有时需要将后端返回的选项值转换为名称。为了解决这个问题,我们可以通过在统一的配置文件中定义字典项,然后在各个页面中使用。

我们先来看看在代码中维护字典的两种方法:

方案1:用代码维护字典常量和名称map

// 1️⃣ 维护字典 src/consts/dicts
/** 性别常量值 */
export const GENDER = {
    male: 1, // 男
    female: 2, // 女
    other: 3, // 其他
}
/** 根据值获取名称map */
export const genderNameMap = {
    [GENDER.male]: '男',
    [GENDER.female]: '女',
    [GENDER.other]: '其他'
}
/**
* 将map转为列表选项
* @param {string} valueType 值的类型
*/
export function mapToOptions(map, valueType) {
    // 值是否为数字
    const isNum = valueType === 'number'
    return Object.keys(map).map(value => {
        return {
            label: map[value],
            value: isNum ? Number(value) : value
        }
    })
}

// 2️⃣ 页面中使用字典项
import { GENDER, genderNameMap, mapToOptions } = '@/consts/dicts'
// 生成字典选项
const genderOptions = mapToOptions(genderNameMap);
// 伪代码,请求获取用户信息
const user = await fetchUser();
// 获取性别对应名称,用于回显
const genderName = genderNameMap[user.gender]
// 判断是否男性
const isMale = user.gender == GENDER.male

方案2:用代码维护字典选项

// 1️⃣ 维护字典 src/consts/dicts
/** 根据选项生成字典方法 */
export function genDict(options) {
    const map = {}
    options.forEach(item => {
        map[item.value] = item
    })
    
    return {
        options, // 字典选项
        getName(value) {
            // 获取名称
            return map[value]?.label
        },
        is(key, value) {
            // 判断
            return map[value]?.key === key
        }
    }
}
}
/** 性别选项 */
export const genderDictOptions = [
    { label: '男', value: 1, key: 'MALE' },
    { label: '女', value: 2, key: 'FEMALE' },
    { label: '其他', value: 3, key: 'OTHER' }
];
/** 性别字典 */
export const genderDict = genDict(genderDictOptions);

// 2️⃣ 页面中使用字典项
import { genderDict } = '@/consts/dicts'
// 生成字典选项
const genderOptions = genderDict.options;
// 伪代码,请求获取用户信息
const user = await fetchUser();
// 获取性别对应名称,用于回显
const genderName = genderDict.getName(user.gender)
// 判断是否男性
const isMale = genderDict.is('MALE', user.gender)

以上两种方法都大差不差,看个人选择:

  1. 第一种用常量 GENDER.male 有代码提示,方便开发,第二种用'MALE'容易写错;
  2. 第二种只需要维护一个选项数组即可,第一种要同时维护一个GENDER常量和一个map对象。

方案3:通过配置维护字典

❗️然而,这两种方法都存在一个缺点,即如果需要修改字典项,就需要修改代码,维护起来不够方便。因此,我们可以通过后台配置字典项并生成列表项,然后前端通过请求接口获取字典项。

通过配置生成字典项,使其更加灵活,但在使用上仍存在一些问题:

  1. 什么时机获取字典项?
    1. 刷新页面时获取所有字典项: 这种方式会导致请求慢,因为字典项可能很多,并且很多字典项并不是每个页面都会用到,这样会浪费请求资源。
    2. 页面按需请求获取字典项: 尽管页面按需请求字典项可以减少不必要的请求,但不同页面或组件可能会重复请求相同的字典项,导致重复请求,增加了服务器负担和网络流量。
    3. 页面按需请求获取字典项,并将获取到的进行缓存: 最佳实践是在页面首次需要使用字典项时,向后端请求获取字典项,并将其缓存起来。如果已经存在缓存,则不进行重复请求。
  2. 如何更高效的将字典值转为名称?

下面以vue项目为例的解决方案:

  1. 统一文件维护字典:src/utils/dict.js
import { fetchDictList } from '@/api' // 字典项请求方法

// 此处用全局变量维护字典项,可以在store里面维护,这样页面中可以直接通过store访问
let _dictMap = {}

/** 初始化项目字典,注册Vue全局对象和mixin */
export function initDict(Vue) {
  // 全局混入字典初始化逻辑,如果组件有dicts属性,先请求获取,无需在页面再手动请求字典项
  Vue.mixin({
    data() {
      const dicts = this.$options?.dicts
      // 如果没有配置dicts属性,不往下执行
      if (!dicts || !dicts.length) return {}
      const nameMap = {}
      const optionsMap = {}
      dicts.forEach(alias => {
        // 默认空对象/数组,减少读取属性时的非空判断
        nameMap[alias] = {} 
        optionsMap[alias] = []
      })
      return {
          // 通过字典值获取字典名称
          // 用法 mixinDictName[字典类型][字典值],如 this.mixinDictName.gender.high
          // 用mixin开头为了提醒变量是从mixin中来
          mixinDictName: nameMap,
          // 字典选项,格式 { 字典类型: 字典选项[] }
          mixinDictOptions: optionsMap,
      }
    },
    async created() {
      // 如果组件的 export default 中有 dicts 选项,初始化时获取字典
      const dicts = this.$options?.dicts
      if (!dicts || !dicts.length) return
      const dictMap = await getDict(this.$options.dicts)
      if (dictMap) {
          // 赋值根据字典类型获取字典选项的对象
          this.mixinDictOptions = dictMap
          // 赋值根据字典值获取字典名称的对象
          const dictNameMap = {}
          Object.keys(dictMap).forEach(key => {
            const nameMap = dictMap[key]?.nameMap
            if (nameMap) {
                dictNameMap[key] = nameMap
            }
          })
          this.mixinDictName = dictNameMap
      }
    }
  })
}

// 存储正在进行的请求的 Promise 对象,避免重复请求
const pendingRequests = {}
/** 请求获取字典项,并缓存已请求过的 */
export async function getDict(originAlias) {
  try {
    let alias = originAlias
    if (typeof alias === 'string') alias = [alias]
    else if (!Array.isArray(alias)) {
      throw new Error('alias 必须为字符串或数组')
    }

    const needFetchDictAlias = [] // 需要请求的字典别名列表

    // 过滤已经请求过的字典
    for (const aItem of alias) {
      // 如果缓存中没有该字典,请求获取
      if (!_dictMap[aItem]) {
        if (pendingRequests[aItem]) {
          // 该字典已在请求中
          await pendingRequests[aItem]
        } else {
          // 该字典未请求
          needFetchDictAlias.push(aItem)
        }
      }
    }

    // 需要请求获取的字典项
    if (needFetchDictAlias.length) {
      const request = fetchAndCacheDict(needFetchDictAlias)
      // 标识字典项正在请求
      needFetchDictAlias.forEach(aItem => {
        pendingRequests[aItem] = request
      })
      const newDictMap = await request
      // 去除正在请求标识
      needFetchDictAlias.forEach(aItem => {
        delete pendingRequests[aItem]
      })
      Object.assign(_dictMap, newDictMap)
    }

    if (typeof originAlias === 'string') return _dictMap[originAlias]
    const result = {}
    for (const aItem of alias) {
      result[aItem] = _dictMap[aItem]
    }
    return result
  } catch (error) {
    console.log('🚀 ~ getDict ~ error:', error)
  }
}

/** 请求并缓存字典项 */
export async function fetchAndCacheDict(alias) {
  const dictMap = {}
  try {    
    // 请求获取字典项
    const res = await fetchDictList({
      alias,
      includeValues: true
    })
    res.list?.forEach((item) => {
      // 将请求结果保存到全局字典项
      dictMap[item.alias] = item.values // 需要返回的字典项
      const dictNameMap = item.values?.reduce((map, dItem) => {
        // 保存字典选项别名对应的名称
        map[dItem.alias] = dItem.title
        return map
      }, {})
      // 给数组加上nameMap属性,用于根据 alias 获取对应名称 title
      dictMap[item.alias].nameMap = dictNameMap
    })
  } catch (error) {
    console.log("🚀 ~ fetchAndCacheDict ~ error:", error)
  }
  return dictMap
}

/** 异步获取字典项对应的名称 */
export async function asyncGetDictName(alias, value) {
  if (!_dictMap[alias]) {
    await getDict(alias)
  }
  return getDictName(alias, value)
}

/** 获取字典项对应的名称,需要请求对应字典后才能使用 */
export function getDictName(alias, value) {
  const nameMap = _dictMap[alias]?.nameMap
  return nameMap[value] || '未知'
}

/** 清除已缓存的字典项,可在合适的时机清空缓存 */
export function clearDict(alias) {
  if (alias) delete _dictMap[alias]
  else _dictMap = {}
}
  1. 在vue页面中使用字典

只需要在页面中配置 dicts 属性,就可以直接使用字典选项,并获取名称,简单快速

<template>
  <!-- 字典值名称回显:读取全局注入的 mixinDictName -->
  <div>{{ mixinDictName['dict_type'][dictValue] }}</div>
  <!-- 字典选项 -->
  <el-select v-model="dictValue">
    <el-option
      v-for="item in mixinDictOptions['dict_type']"
      :key="item.alais"
      :label="item.label"
      :value="item.alias" />
  </el-select>
</template>
<script>
export default {
  dicts: ['dict_type'], // 需要使用到的字典类型/别名
  data() {
    return {
      dictValue: 1 // 字典项的值
    }
  },
  // ...
}

如果页面有请求先后的需求或其他特殊需求,也可以直接调用getDict方法

import { getDict } from '@/utils/dict'
const options = await getDict('dict_type')
// ...执行其他操作

全文完,望批评指正。