# 前端数据字典的最优方案探索
# 什么是数据字典
字典:字典是一个键值对,主要的特征是一一对应,字典中的 key 是不能重复且无序的,value 可以重复。 key 用于在前后端的传输或者在代码中做逻辑判断,value 用于向用户展示。
# 为什么要在后端维护字典表
其实前端如果非要自己维护也是可以的,这样前端开发人员在代码上确实可以省掉很多事情,我曾经也这样考虑过,但是这样有几个缺陷确实让人难以接受,所以我还是屈服了。
- 字典表必须永远和后端保持一致,维护难度太大,稍不注意前后端就可能对不上。
- 虽然字典不常修改但是不代表永远不会修改,如果一有改动,前端就要改代码,打包,部署。。。
- 如果出现1中的问题,前端可能会背锅(并且还甩不掉)。
需要知道的是,不管字典在哪里维护,如果代码中有逻辑判断,字典修改后都是需要改代码的,前后端的都是。
# 在后端维护的字典表前端应该怎么样合理使用
好了,现在字典全给后端维护了,以后前端都不会背字典的锅了。现在我们来看看怎么使用字典,通常情况下会有这几种使用方式。
- 在下拉选择器(select)中使用。
- 将后端传过来的key转成value显示(一般在表格中)。
- 逻辑判断。
对于第一种使用方式,我们一般会写成一个公共组件,如:DictSelect
,它会接受一个参数(dictType),在加载组件的时候通过 dictType 获取当前字典的所有选项。我们知道,不管是vue还是react,一个组件在多次使用时都是不同的实例,也就是说我每次使用DictSelect
组件时都会向后端发起一个请求。想象一下,如果在一个页面中,有两个性别选择器,当我们使用DictSelect
时,一进入页面就会向后端发起两次获取性别字典的请求,他们的结果还都是一样的。假如页面上还有个弹窗,弹窗里也有个性别选择器,弹窗打开时又会发起一次请求。假如我现在又要跳转到另外一个页面。。。
其实这种情况你要是不去在意它它就不是一个问题,它的问题也只是多发了几次请求而已,不仅是这个字典选择器有这个问题,其他的异步选择器也是这样。
现在来看第二种使用方式,这种方式一般在表格里面,当我获取表格数据的时候,假如里面有一项数据代表性别,后端给我的是一个 key 值,我在展示的时候需要将这个 key 转成 value。这个问题我最先想到的解决办法是vue的过滤器,但是过滤器又不能支持异步,如果非要使用过滤器的话(函数也一样),我必须在每次使用前先初始化字典数据,但是这样我就没法完全将字典相关代码从业务中脱离出来。(我花了很长时间寻找过滤器的异步方式,现在已经放弃了,根本没有。)
假如我使用一个组件。但是这样的话就和DictSelect
一样了,假如表格页大小为10,那它就会同时发起10条相同的请求,这是真的接受不了。
第三种情况,没有合适办法,只能在代码中写死。
# 解决方案
我封装了一个专门用来请求字典的函数, 他有两个逻辑:
- 每次发起请求时将当前请求的 Promise 存起来,请求结束后将Promise改为null,每次发起请求时先判断Promise是否存在,如果有就直接返回。这样可以处理那些同一时刻发起多个请求的问题。
- 发起请求之前先判断 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, // 是否刷新
});
现在再结合组件使用就完美了。