前言
在一些项目里,我们可能有着大量的下拉框,而这些下拉框的数据就来源于我们后端接口返回的字典信息。于是,画风可能是这样的,每次下拉,你都需要请求一次字典接口拿到这些数据,于是每次组件刷新都会重复请求接口,造成性能上的浪费,如下图所示:
那么,我们可不可以把这些字典值缓存起来,只在第一次加载时请求一次呢?
使用vuex缓存字典数据
在Vue
项目中,我们要实现数据的缓存,自然要考虑到Vuex
、Pinia
状态管理了。
思路是这样的:每个字典肯定有一个key,也就是其名字,每次拿字典值先从状态管理中寻找字典的key,如果找不到或者数据为空,我们就请求接口,响应后先把数据处理后存放在状态管理后再返回;当第二次请求时,自然地就从状态管理中取值而不是再次请求接口了。
新建测试项目,后台使用koa
返回简单的字典值:
前端为Vue3
+Vuex
,使用Element-Plus
组件库中的Select
下拉组件,此下拉组件需要的数据格式如下:
[{label:"",value:""}]
Vuex
中相关代码为:
const store =createStore({
state:{
dicData:{
}
},
mutations:{
getDic(state:any,key:string){
return state.dicData[key]
},
setDic(state:any,data:{key:string;list:any[]}){
const {key,list} =data;
state.dicData[key]=list
}
},
actions:{
async GET_DIC(context:any,payload:any){
const {key} = payload
if(context.state.dicData[key]){
return context.state.dicData[key]
} else{
const data =(await getDicData(key)).data
const list = data.map(item=>({
value: item.key || item.code,
label: item.value || item.name
}))
context.commit('setDic',{key:key,list:list});
return list
}
}
}
})
在actions的GET_DIC
方法中,我们根据传入的key先从state中的dicData
取出字典值,取不到后才请求接口getDicData
拿到值,做数据处理后,先通过mutation的setDic
方法设置字典数据再返回。
调用如下:
store.dispatch("GET_DIC",{key:"fruit"}).then(res=>{
options.value=res;
})
这样,无论组件怎么刷新接口始终只调用一次,节省了等待时间和带宽。
如果后端接口没有这么规范,每一个字典都单独做了一个接口,也可以利用swtich
,根据不同的key单独写处理数据的逻辑,最终在dicData
的值均是形如以下的数据格式就好了。
dicData:{
"fruit":[{label:"苹果",value:101}],
"class":[{label:"一年级一班",value:201}]
}
Pinia的实现
export const useSelectStore =defineStore('select',{
state:()=>{
return{
dicData:{}
}
},
actions:{
setDic(key:string,list:any[]){
this.dicData[key]=list;
},
async getDic(key:string){
console.log(this.dicData)
if(this.dicData[key]){
return this.dicData[key]
} else{
const data =(await getDicData(key)).data;
const list = data.map(item=>({
value: item.key || item.code,
label: item.value || item.name
}));
this.setDic(key,list);
return list;
}
}
}
})
调用
const selectStore= useSelectStore();
selectStore.getDic(props.requestKey).then(res=>{
options.value=res;
})
Pinia
代码上差不了太多,不过其本身的确比Vuex
简洁一些。
同时渲染造成的请求并发问题
评论区有小伙伴说到了万一同时渲染会发起多个一样的请求,也就是请求并发的问题。如图所示,页面有四个班级列表的下拉。渲染该页面则每个下拉都会发起一个请求,即:
可以看到,班级的字典数据请求共有4次,违背了我们设计的初衷。那么,我们如何解决呢?就要对接口本身做缓存了,而这个缓存方案也很简单,我们这块只需要对字典数据的接口单独缓存,使用Promise
就够了。
先上代码(以Pinia为例):
const cacheMap={};
export const useSelectStore =defineStore('select',{
state:()=>{
return{
dicData:{}
}
},
actions:{
setDic(key:string,list:any[]){
this.dicData[key]=list;
},
getDic(key:string){
// console.log(this.dicData)
if(this.dicData[key]){
return new Promise((resolve,reject)=>{
resolve(this.dicData[key])
})
} else{
// return new Promise((resolve,reject)=>{
// getDicData(key).then(res=>{
// const data =res.data;
// const list = data.map(item=>({
// value: item.key || item.code,
// label: item.value || item.name
// }));
// this.setDic(key,list);
// resolve(list);
// })
// })
//添加接口并发缓存处理
if((cacheMap?.[key]?.length ?? 0)==0){
cacheMap[key]=[];
getDicData(key).then(res=>{
const data =res.data;
while(cacheMap[key].length){
const list = data.map(item=>({
value: item.key || item.code,
label: item.value || item.name
}));
this.setDic(key,list);
const resolve = cacheMap[key].shift();
resolve[0](list)
}
})
}
return new Promise((resolve,reject)=>{
cacheMap[key].push([resolve,reject]);
})
}
}
}
})
cacheMap
为全局的缓存对象,其每个属性key
为字典的key,每次请求返回一个promise
,并将resolve
和reject
回调存储下来。对于cacheMap[key]
,其数组为空时,可知是首次请求,此时调用接口获取数据。由于接口请求也是异步,实例化Promise
的语句是同步的,当请求响应前所有的resolve
与reject
回调已经缓存。我们调用所有的resolve
完成所有的promise
并清空数组,就实现了简单的接口缓存。