前端数据字典的最优方案探索
前置说明
前端时间帮一个公司项目做性能调优,发现该公司的项目数据字典的使用方式比较陈旧,数据字典的使用不当对数据库造成的压力非常大。主要在以下几个场景下存在问题:
- 前端页面每一个下拉框,就发送一ajax请求进行加载数据字典。如果一个页面有十几个下拉框,并且用户并发量大的情况下,对于后端服务还是有一定的压力的。
- 前端数据表格中的字段字典value,需要翻译成字典label。有些旧的系统使用SQL将数据表关联字典表,将数据字典的value和label随着数据记录一并返回。一个表有5个字段需要数据字典翻译,就要关联字典表5次。这种SQL无论无论后期维护,还是数据库性能方面都是灾难。
可不可以一次性加载?
那么就涉及到一个问题?数据字典一个一个ajax请求的加载方式,基本上我们可以排除了。那我们就想到另外一个方案,在前端登陆之后将系统所有的数据字典缓存到前端。这种方式稍微好一点,但是这真的是一个超级大的系统,数据字典有上万条数据,如果一次性加载到前端,用户体验可想而知。显然这样也是我们不能忍受的。
解决方案:按需加载
今天在浏览技术文章的时候,看到这样一篇文章《前端数据字典的最优方案探索》。它采用的是按需加载的方式。就是第一次使用一个数据字典就发送请求加载这个数据字典,并将该数据字典放入浏览器本地缓存中。第二次再用到这个字段的时候,就不再发送网络请求,而是从浏览器本地获取数据字典的值。
这种方案能够解决一个问题:一个浏览器用户所需的数据字典只加载一次,而且是按需加载,不需要的不加载。这种分次加载的方式,用户体验也没问题。
如何处理高并发的问题
前端解决的是一个用户的数据字典反复加载的问题,处理完前端我们来看后端。通过配合后端redis缓存设计,数据字典可以将redis缓存与数据库保持一致性,进而多用户高并发查询数据字典先落到redis缓存中,redis缓存中不存在才去查询数据库,从而避免对数据库造成的压力问题。
下面的内容为转载内容:《前端数据字典的最优方案探索》 :https://juejin.cn/post/6949080259438313509
一、什么是数据字典
字典:字典是一个键值对,主要的特征是一一对应,字典中的 key 是不能重复且无序的,value 可以重复。 key 用于在前后端的传输或者在代码中做逻辑判断,value 用于向用户展示。
二、为什么要在后端维护字典表
其实前端如果非要自己维护也是可以的,这样前端开发人员在代码上确实可以省掉很多事情,我曾经也这样考虑过,但是这样有几个缺陷确实让人难以接受,所以我还是屈服了。
- 字典表必须永远和后端保持一致,维护难度太大,稍不注意前后端就可能对不上。
- 虽然字典不常修改但是不代表永远不会修改,如果一有改动,前端就要改代码,打包,部署。。。
- 如果出现1中的问题,前端可能会背锅(并且还甩不掉)。
需要知道的是,不管字典在哪里维护,如果代码中有逻辑判断,字典修改后都是需要改代码的,前后端的都是。
三、在后端维护的字典表前端常规使用方法
好了,现在字典全给后端维护了,以后前端都不会背字典的锅了。现在我们来看看怎么使用字典,通常情况下会有这几种使用方式。
3.1. 在下拉选择器(select)中使用。
对于第一种使用方式,我们一般会写成一个公共组件,如:DictSelect ,它会接受一个参数(dictType),在加载组件的时候通过 dictType 获取当前字典的所有选项。我们知道,不管是vue还是react,一个组件在多次使用时都是不同的实例,也就是说我每次使用DictSelect组件时都会向后端发起一个请求。想象一下,如果在一个页面中,有两个性别选择器,当我们使用DictSelect时,一进入页面就会向后端发起两次获取性别字典的请求,他们的结果还都是一样的。假如页面上还有个弹窗,弹窗里也有个性别选择器,弹窗打开时又会发起一次请求。假如我现在又要跳转到另外一个页面。。。
其实这种情况你要是不去在意它它就不是一个问题,它的问题也只是多发了几次请求而已,不仅是这个字典选择器有这个问题,其他的异步选择器也是这样。
3.2. 将后端传过来的key转成value显示(一般在表格中)。
现在来看第二种使用方式,这种方式一般在表格里面,当我获取表格数据的时候,假如里面有一项数据代表性别,后端给我的是一个 key 值,我在展示的时候需要将这个 key 转成 value。这个问题我最先想到的解决办法是vue的过滤器,但是过滤器又不能支持异步,如果非要使用过滤器的话(函数也一样),我必须在每次使用前先初始化字典数据,但是这样我就没法完全将字典相关代码从业务中脱离出来。(我花了很长时间寻找过滤器的异步方式,现在已经放弃了,根本没有。)
假如我使用一个组件。但是这样的话就和DictSelect一样了,假如表格页大小为10,那它就会同时发起10条相同的请求,这是真的接受不了。
3.3.逻辑判断
第三种情况,没有合适办法,只能在代码中写死。
四、解决方案
我封装了一个专门用来请求字典的函数, 他有两个逻辑:
每次发起请求时将当前请求的 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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
import getAxios from "@/utils/dict-cache.js"; // 接口缓存
const dictRequest = (type, refresh = false) =>
getAxios(`/admin/dict/type/${type}`, {
cache: true, // 是否开启缓存
repeat: true, // 是否开启防止同时发起多个相同请求
refresh: refresh, // 是否刷新
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
现在再结合组件使用就完美了。
文章来源: zimug.blog.csdn.net,作者:字母哥哥,版权归原作者所有,如需转载,请联系作者。
原文链接:zimug.blog.csdn.net/article/details/122997675
- 点赞
- 收藏
- 关注作者
评论(0)