# 前端数据字典的最优方案探索

# 什么是数据字典

字典:字典是一个键值对,主要的特征是一一对应,字典中的 key 是不能重复且无序的,value 可以重复。 key 用于在前后端的传输或者在代码中做逻辑判断,value 用于向用户展示。

# 为什么要在后端维护字典表

其实前端如果非要自己维护也是可以的,这样前端开发人员在代码上确实可以省掉很多事情,我曾经也这样考虑过,但是这样有几个缺陷确实让人难以接受,所以我还是屈服了。

  1. 字典表必须永远和后端保持一致,维护难度太大,稍不注意前后端就可能对不上。
  2. 虽然字典不常修改但是不代表永远不会修改,如果一有改动,前端就要改代码,打包,部署。。。
  3. 如果出现1中的问题,前端可能会背锅(并且还甩不掉)。

需要知道的是,不管字典在哪里维护,如果代码中有逻辑判断,字典修改后都是需要改代码的,前后端的都是。

# 在后端维护的字典表前端应该怎么样合理使用

好了,现在字典全给后端维护了,以后前端都不会背字典的锅了。现在我们来看看怎么使用字典,通常情况下会有这几种使用方式。

  1. 在下拉选择器(select)中使用。
  2. 将后端传过来的key转成value显示(一般在表格中)。
  3. 逻辑判断。

对于第一种使用方式,我们一般会写成一个公共组件,如:DictSelect ,它会接受一个参数(dictType),在加载组件的时候通过 dictType 获取当前字典的所有选项。我们知道,不管是vue还是react,一个组件在多次使用时都是不同的实例,也就是说我每次使用DictSelect组件时都会向后端发起一个请求。想象一下,如果在一个页面中,有两个性别选择器,当我们使用DictSelect时,一进入页面就会向后端发起两次获取性别字典的请求,他们的结果还都是一样的。假如页面上还有个弹窗,弹窗里也有个性别选择器,弹窗打开时又会发起一次请求。假如我现在又要跳转到另外一个页面。。。

其实这种情况你要是不去在意它它就不是一个问题,它的问题也只是多发了几次请求而已,不仅是这个字典选择器有这个问题,其他的异步选择器也是这样。

现在来看第二种使用方式,这种方式一般在表格里面,当我获取表格数据的时候,假如里面有一项数据代表性别,后端给我的是一个 key 值,我在展示的时候需要将这个 key 转成 value。这个问题我最先想到的解决办法是vue的过滤器,但是过滤器又不能支持异步,如果非要使用过滤器的话(函数也一样),我必须在每次使用前先初始化字典数据,但是这样我就没法完全将字典相关代码从业务中脱离出来。(我花了很长时间寻找过滤器的异步方式,现在已经放弃了,根本没有。)

假如我使用一个组件。但是这样的话就和DictSelect一样了,假如表格页大小为10,那它就会同时发起10条相同的请求,这是真的接受不了。

第三种情况,没有合适办法,只能在代码中写死。

# 解决方案

我封装了一个专门用来请求字典的函数, 他有两个逻辑:

  1. 每次发起请求时将当前请求的 Promise 存起来,请求结束后将Promise改为null,每次发起请求时先判断Promise是否存在,如果有就直接返回。这样可以处理那些同一时刻发起多个请求的问题。
  2. 发起请求之前先判断 Loca Storage 中是否有缓存,如果有直接取出来,没有的话再发起请求,成功之后会先缓存一份。
/**
 * 对get请求进行包装
 * 提供数据缓存和防止同时发起相同的请求
 * 相同的路径就可以理解为相同的请求
 */
import request from "@/utils//axios";

const promiseRecord = {}; // 用于缓存请求状态

/**
 * 通过路径和参数生成唯一字符
 * @param {*} apiUrl
 */
const createKey = (apiUrl) => {
  return apiUrl;
};

// 普通的 get 请求
const get = (apiUrl) => request.get(apiUrl)

/**
 * 用来发起需要缓存的请求
 * @param {String} apiUrl
 * @param {Boolean} refresh 可能在某些情况下不能使用缓存必须到后台获取
 */
const getCache = (apiUrl, refresh = false) => {
  // 用请求路径和参数生成标识,完全相同的请求的标识一样,作为储存的键
  let keyName = createKey(apiUrl);

  return new Promise((resolve, reject) => {
    let data = sessionStorage.getItem(keyName);

    let request = () => {
      get(apiUrl)
        .then((value) => {
          sessionStorage.setItem(keyName, JSON.stringify(value));
          resolve(value);
        })
        .catch((error) => {
          reject(error);
        });
    };

    if (data && !refresh) {
      // 如果用户手动修改了 sessionStorage 里的数据可能会出错,应该做下处理
      try {
        resolve(JSON.parse(data));
      } catch (e) {
        request();
      }
    } else {
      request();
    }
  });
};

/**
 * 防止重复处理
 */
const repeat = (apiUrl, request, refresh) => {
  // 用请求路径和参数生成标识,完全相同的请求的标识一样,可以使用同一个请求结果
  let keyName = createKey(apiUrl);

  if (!promiseRecord[keyName]) {
    promiseRecord[keyName] = new Promise((resolve, reject) => {
      request(apiUrl, refresh)
        .then((value) => {
          promiseRecord[keyName] = null;
          resolve(value);
        })
        .catch((error) => {
          promiseRecord[keyName] = null;
          reject(error);
        });
    });
  }

  return promiseRecord[keyName];
};

/**
 * 返回请求的函数
 * @param {String} apiUrl
 * @param {Object} options 配置项
 */
const getAxios = (apiUrl, options = {}) => {
  // 默认配置
  let defaults = {
    cache: false, // 是否开启缓存
    repeat: false, // 是否开启防止同时发起相同的请求
    refresh: false, // 是否刷新(这里也不能保证会刷新,因为get也有缓存,只能保证它会发出请求)
  };
  let _options = Object.assign(Object.assign({}, defaults), options);

  // 什么都不需要 返回原始的axiso get请求
  if (!_options.cache && !_options.repeat) {
    return get(apiUrl);
  }

  // 只需要缓存
  if (_options.cache && !_options.repeat) {
    return getCache(apiUrl, _options.refresh);
  }

  // 只需要防止同时发起相同的请求
  if (!_options.cache && _options.repeat) {
    return repeat(apiUrl, get);
  }

  // 小孩子才做选择,大人全都要
  if (_options.cache && _options.repeat) {
    return repeat(apiUrl, getCache, _options.refresh);
  }
};

export default getAxios;

import getAxios from "@/utils/dict-cache.js"; // 接口缓存

const dictRequest = (type, refresh = false) =>
  getAxios(`/admin/dict/type/${type}`, {
    cache: true, // 是否开启缓存
    repeat: true, // 是否开启防止同时发起多个相同请求
    refresh: refresh, // 是否刷新
  });

现在再结合组件使用就完美了。

上次更新:: 4/9/2021, 3:22:42 PM