设计一个支持并发的前端缓存接口

文章目录

  • 一、概述
  • 二、并发缓存
    • 2.1、问题
    • 2.2、思考
    • 2.3、优化
  • 三、总结
  • 四、最后

一、概述

缓存池不过就是一个map,存储接口数据的地方,将接口的路径和参数拼到一块作为key,数据作为value存起来罢了,这个咱谁都会。

const cacheMap = new Map();

封装一下调用接口的方法,调用时先走咱们缓存数据。

import axios, { AxiosRequestConfig } from 'axios'// 先来一个简简单单的发送
export function sendRequest(request: AxiosRequestConfig) {return axios(request)
}

然后加上咱们的缓存

import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'const cacheMap = new Map()interface MyRequestConfig extends AxiosRequestConfig {needCache?: boolean
}// 这里用params是因为params是 GET 方式穿的参数,我们的缓存一般都是 GET 接口用的
function generateCacheKey(config: MyRequestConfig) {return config.url + '?' + qs.stringify(config.params)
}export function sendRequest(request: MyRequestConfig) {const cacheKey = generateCacheKey(request)// 判断是否需要缓存,并且缓存池中有值时,返回缓存池中的值if (request.needCache && cacheMap.has(cacheKey)) {return Promise.resolve(cacheMap.get(cacheKey))}return axios(request).then((res) => {// 这里简单判断一下,200就算成功了,不管里面的data的code啥的了if (res.status === 200) {cacheMap.set(cacheKey, res.data)}return res})
}

然后调用

const getArticleList = (params: any) =>sendRequest({needCache: true,url: '/article/list',method: 'get',params})getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})

这个部分就很简单,我们在调接口时给一个needCache的标记,然后调完接口如果成功的话,就会将数据放到cacheMap中去,下次再调用的话,就直接返回缓存中的数据。

二、并发缓存

上面的虽然看似实现了缓存,不管我们调用几次,都只会发送一次请求,剩下的都会走缓存。但是真的是这样吗?

getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})
getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})

其实这样,就可以测出,我们的虽然设计了缓存,但是请求还是发送了两次,这是因为我们第二次请求发出时,第一次请求还没完成,也就没给缓存池里放数据,所以第二次请求没命中缓存,也就又发了一次。

2.1、问题

那么,有没有一种办法让第二次请求等待第一次请求调用完成,然后再一块返回呢?

2.2、思考

有了!我们写个定时器就好了呀,比如我们可以给第二次请求加个定时器,定时器时间到了再去cacheMap中查一遍有没有缓存数据,没有的话可能是第一个请求还没好,再等几秒试试!

可是这样的话,第一个请求的时候也会在原地等呀!😒

那这样的话,让第一个请求在一个地方贴个告示不就好了,就像上厕所的时候在门口挂个牌子一样!😎

// 存储缓存当前状态,相当于挂牌子的地方
const statusMap = new Map<string, 'pending' | 'complete'>();export function sendRequest(request: MyRequestConfig) {const cacheKey = generateCacheKey(request)// 判断是否需要缓存if (request.needCache) {if (statusMap.has(cacheKey)) {const currentStatus = statusMap.get(cacheKey)// 判断当前的接口缓存状态,如果是 complete ,则代表缓存完成if (currentStatus === 'complete') {return Promise.resolve(cacheMap.get(cacheKey))}// 如果是 pending ,则代表正在请求中,这里就等个三秒,然后再来一次看看情况if (currentStatus === 'pending') {return new Promise((resolve, reject) => {setTimeout(() => {sendRequest(request).then(resolve, reject)}, 3000)})}}statusMap.set(cacheKey, 'pending')}return axios(request).then((res) => {// 这里简单判断一下,200就算成功了,不管里面的data的code啥的了if (res.status === 200) {statusMap.set(cacheKey, 'complete')cacheMap.set(cacheKey, res)}return res})
}

试试效果

getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})
getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})

img

img

成了!这里真的做到了,可以看到我们这里打印了两次,但是只发了一次请求。

2.3、优化

可是用setTimeout等待还是不太优雅,如果第一个请求能在3s以内完成还行,用户等待的时间还不算太久,还能忍受。可如果是3.1s的话,第二个接口用户可就白白等了6s之久,那么,有没有一种办法,能让第一个接口完成后,接着就通知第二个接口返回数据呢?

等待,通知,这种场景我们写代码用的最多的就是回调了,但是这次用的是promise啊,而且还是毫不相干的两个promise。等等!callbackpromisepromise本身就是callback实现的!promisethen会在resole被调用时调用,这样的话,我们可以将第二个请求的resole放在一个callback里,然后在第一个请求完成的时候,调用这个callback!🥳

// 定义一下回调的格式
interface RequestCallback {onSuccess: (data: any) => voidonError: (error: any) => void
}// 存放等待状态的请求回调
const callbackMap = new Map<string, RequestCallback[]>()export function sendRequest(request: MyRequestConfig) {const cacheKey = generateCacheKey(request)// 判断是否需要缓存if (request.needCache) {if (statusMap.has(cacheKey)) {const currentStatus = statusMap.get(cacheKey)// 判断当前的接口缓存状态,如果是 complete ,则代表缓存完成if (currentStatus === 'complete') {return Promise.resolve(cacheMap.get(cacheKey))}// 如果是 pending ,则代表正在请求中,这里放入回调函数if (currentStatus === 'pending') {return new Promise((resolve, reject) => {if (callbackMap.has(cacheKey)) {callbackMap.get(cacheKey)!.push({onSuccess: resolve,onError: reject})} else {callbackMap.set(cacheKey, [{onSuccess: resolve,onError: reject}])}})}}statusMap.set(cacheKey, 'pending')}return axios(request).then((res) => {// 这里简单判断一下,200就算成功了,不管里面的data的code啥的了if (res.status === 200) {statusMap.set(cacheKey, 'complete')cacheMap.set(cacheKey, res)} else {// 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求statusMap.delete(cacheKey)}// 这里触发resolve的回调函数if (callbackMap.has(cacheKey)) {callbackMap.get(cacheKey)!.forEach((callback) => {callback.onSuccess(res)})// 调用完成之后清掉,用不到了callbackMap.delete(cacheKey)}return res},(error) => {// 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求statusMap.delete(cacheKey)// 这里触发reject的回调函数if (callbackMap.has(cacheKey)) {callbackMap.get(cacheKey)!.forEach((callback) => {callback.onError(error)})// 调用完成之后也清掉callbackMap.delete(cacheKey)}// 这里要返回 Promise.reject(error),才能被catch捕捉到return Promise.reject(error)})
}

在判断到当前请求状态是pending时,将promiseresolereject放入回调队列中,等待被触发调用。然后在请求完成时,触发对应的请求队列。

试一下

getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})
getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})

img

img

再试一下失败的时候

getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)},(error) => {console.error(error)}
)
getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)},(error) => {console.error(error)}
)

img

OK,两个都失败了。(但是这里的error2早于error1打印,你知道是啥原因吗?🤔)

三、总结

promise封装并发缓存到这里就结束啦,不过看到这里你可能会觉着没啥用处,但是其实这也是我碰到的一个需求才延申出来的,当时的场景是一个页面里有好几个下拉选择框,选项都是接口提供的常量。但是只接口提供了一个接口返回这些常量,前端拿到以后自己再根据类型挑出来,所以这种情况我们肯定不能每个下拉框都去调一次接口,只能是寄托缓存机制了。

这种写法,在另一种场景下也很好用,比如将需要用户操作的流程封装成promise。例如,A页面点击A按钮,出现一个B弹窗,弹窗里有B按钮,用户点击B按钮之后关闭弹窗,再弹出C弹窗C按钮,点击C之后流程完成,这种情况就很适合将每个弹窗里的操作流程都封装成一个promise,最外面的A页面只需要连着调用这几个promise就可以了,而不需要维护控制这几个弹窗显示隐藏的变量了。

对请求结果是否成功那里处理的比较简陋,项目里用到的话根据自己情况来。

放一下全部代码

import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'// 存储缓存数据
const cacheMap = new Map()// 存储缓存当前状态
const statusMap = new Map<string, 'pending' | 'complete'>()// 定义一下回调的格式
interface RequestCallback {onSuccess: (data: any) => voidonError: (error: any) => void
}// 存放等待状态的请求回调
const callbackMap = new Map<string, RequestCallback[]>()interface MyRequestConfig extends AxiosRequestConfig {needCache?: boolean
}// 这里用params是因为params是 GET 方式穿的参数,我们的缓存一般都是 GET 接口用的
function generateCacheKey(config: MyRequestConfig) {return config.url + '?' + qs.stringify(config.params)
}export function sendRequest(request: MyRequestConfig) {const cacheKey = generateCacheKey(request)// 判断是否需要缓存if (request.needCache) {if (statusMap.has(cacheKey)) {const currentStatus = statusMap.get(cacheKey)// 判断当前的接口缓存状态,如果是 complete ,则代表缓存完成if (currentStatus === 'complete') {return Promise.resolve(cacheMap.get(cacheKey))}// 如果是 pending ,则代表正在请求中,这里放入回调函数if (currentStatus === 'pending') {return new Promise((resolve, reject) => {if (callbackMap.has(cacheKey)) {callbackMap.get(cacheKey)!.push({onSuccess: resolve,onError: reject})} else {callbackMap.set(cacheKey, [{onSuccess: resolve,onError: reject}])}})}}statusMap.set(cacheKey, 'pending')}return axios(request).then((res) => {// 这里简单判断一下,200就算成功了,不管里面的data的code啥的了if (res.status === 200) {statusMap.set(cacheKey, 'complete')cacheMap.set(cacheKey, res)} else {// 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求statusMap.delete(cacheKey)}// 这里触发resolve的回调函数if (callbackMap.has(cacheKey)) {callbackMap.get(cacheKey)!.forEach((callback) => {callback.onSuccess(res)})// 调用完成之后清掉,用不到了callbackMap.delete(cacheKey)}return res},(error) => {// 不成功的情况下删掉 statusMap 中的状态,能让下次请求重新请求statusMap.delete(cacheKey)// 这里触发reject的回调函数if (callbackMap.has(cacheKey)) {callbackMap.get(cacheKey)!.forEach((callback) => {callback.onError(error)})// 调用完成之后也清掉callbackMap.delete(cacheKey)}return Promise.reject(error)})
}const getArticleList = (params: any) =>sendRequest({needCache: true,baseURL: 'http://localhost:8088',url: '/article/blogList',method: 'get',params})export function testApi() {getArticleList({page: 1,pageSize: 10}).then((res) => {console.log(res)},(error) => {console.error('error1:', error)})getArticleList({page: 1,pageSize: 10}).then((res) => {console.log(res)},(error) => {console.error('error2:', error)})
}

四、最后

本人每篇文章都是一字一句码出来,希望对大家有所帮助,多提提意见。顺手来个三连击,点赞👍收藏💖关注✨,一起加油☕

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/249323.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pnpm tauri icon 一键转换 icon

在使用 Electron、Tauri 等框架开发桌面应用时&#xff0c;需要为应用生成不同平台的图标&#xff0c;如&#xff1a;MacOS 中的 icon.icns、Windows 中的 icon.ico、Linux 中的 *.png 等类型图标&#xff0c;这里介绍一种简单快捷一键转换图标的方法。 准备工作 nodejs 环境n…

ElasticSearch 应用实践 笔记

概述 介绍 ES 是一个开源的高扩展的分布式全文搜索引擎&#xff0c;是整个Elastic Stack技术栈的核心。它可以近乎实时的存储&#xff0c;检索数据&#xff1b;本身扩展性很好&#xff0c;可以扩展到上百台服务器&#xff0c;处理PB级别的数据。ElasticSearch的底层是开源库Lu…

c JPEG ZRL (15,0) 的问题

问题&#xff1a;如果量化表全为64个1&#xff0c;编码图片显示非常乱。如果用压缩比较大的量化表&#xff0c;显示基本正常。如果编码过程中不处理ZRL&#xff0c;图片正常。此问题一直没有排除。 下图为两张量化表全为64个1的情况下加了&#xff08;15,0&#xff09;后的不正…

[Bug] [OpenAI] [TypeError: fetch failed] { cause: [Error: AggregateError] }

[Bug] [OpenAI] [TypeError: fetch failed] { cause: [Error: AggregateError] } ubuntu20 win10 edge浏览器访问 服务器部署 页面打开后想使用chatgpt报错了 rootcoal-pasi1cmp:/www/wwwroot/ChatGPT-Next-Web# PORT3000 yarn start yarn run v1.22.19 warning package.json:…

实现vue3响应式系统核心-shallowReactive

简介 今天来实现一下 shallowReactive 这个 API。 reactive函数是一个深响应&#xff0c;当你取出的值为对象类型&#xff0c;需要再次调用 reactive进行响应式处理。很明显我们目前的代码是一个浅响应&#xff0c;即 只代理了对象的第一层&#xff0c;也就是 shallowReactiv…

【方法论】费曼学习方法

费曼学习方法是由诺贝尔物理学奖得主理查德费曼提出的一种学习方法。这种方法强调通过将所学的知识以自己的方式解释给别人来提高学习效果。 费曼学习方法的步骤如下&#xff1a; 选择一个概念&#xff1a;选择一个要学习的概念或主题。 理解和学习&#xff1a;用自己的方式学…

TCP四次握手

TCP 协议在关闭连接时&#xff0c;需要进行四次挥手的过程&#xff0c;主要是为了确保客户端和服务器都能正确地关闭连接。 # 执行流程 四次挥手的具体流程如下&#xff1a; 客户端发送 FIN 包&#xff1a;客户端发送一个 FIN 包&#xff0c;其中 FIN 标识位为 1&#xff0c…

MATLAB - 仿真单摆的周期性摆动

系列文章目录 前言 本例演示如何使用 Symbolic Math Toolbox™ 模拟单摆的运动。推导摆的运动方程&#xff0c;然后对小角度进行分析求解&#xff0c;对任意角度进行数值求解。 一、步骤 1&#xff1a;推导运动方程 摆是一个遵循微分方程的简单机械系统。摆最初静止在垂直位置…

阿赵UE学习笔记——14、LOD

阿赵UE学习笔记目录   大家好&#xff0c;我是阿赵。   继续学习虚幻引擎的用法。这次看看虚幻引擎的Level Of Detail(LOD)的用法。 一、测试场景准备 用植物系统&#xff0c;在地形上面刷了好多草&#xff1a; 这个时候看一下网格&#xff0c;会发现网格比较多和密集。 …

Leetcode第382场周赛

Leetcode第382场周赛 本人水平有限&#xff0c;只做前三道。 一、按键变更的次数 给你一个下标从 0 开始的字符串 s &#xff0c;该字符串由用户输入。按键变更的定义是&#xff1a;使用与上次使用的按键不同的键。例如 s “ab” 表示按键变更一次&#xff0c;而 s “bBBb”…

tableau绘制雷达图

目标图形: 1. 数据准备 &#xff08;1&#xff09;原始数据 你要进行用雷达图比较的对象的各指标的数据。 (2) 处理后数据 在原数据的基础上添加对各指标进行区间的划分数据&#xff0c;也就是层级的划分。 2. 操作步骤 &#xff08;1&#xff09;数据转化 转化前&#xf…

Logstash 7.7.1版本安装系统梳理

前言 上一篇文章介绍了 《ElasticSearch7.7.1集群搭建 & Kibana安装》&#xff0c;今天说一下 Logstash的安卓和配置&#xff1b; Logstash是一个开源的数据收集引擎&#xff0c;具有实时管道功能。它可以动态地将来自不同数据源的数据统一起来&#xff0c;并将数据标准化…

idea docker 镜像生成太慢太大问题

文章目录 前言一、更小的jdk基础镜像二、服务瘦包&#xff08;thin jar&#xff09;2.1 maven2.2 修改dockerfile2.3 container run options 三、 基础jdk镜像入手&#xff1f;总结 前言 idea docker 内网应用实践遗留问题 idea docker插件 build 服务镜像太慢服务镜像太大 …

补充推导步骤,重写 Matrix Computations 5.1.2 节

本来的内容有点小小的跳跃&#xff0c;补一下跳跃的部分&#xff0c;下次推导时省点时间&#xff0c;备忘 1. 补充后的内容 2. 代码 LaTeX code&#xff1a; \documentclass{article} \title{Matrix Computations 5.1.2 time saving revision} \date{} \begin{document} \mak…

CSRF靶场练习

简述&#xff1a;CSRF漏洞实际很少&#xff1b;条件限制很多&#xff1b;局限性很大&#xff1b;实验仅供参考&#xff0c;熟悉csrf概念和攻击原理即可 Pikachu靶场 CSRF GET 登录用户vince的账户可以看到用户的相关信息&#xff1b; 点击修改个人信息&#xff0c;发现数据包…

[office] excel2010双向条形图制作 #经验分享#微信

excel2010双向条形图制作 本教程为大家介绍一下excel2010中excel2010双向条形图制作方法。 1.选中工作区域 2.点击插入-->图表,选择条形图 3.为美观可将中间竖线可去掉 4.方法是选中竖线,右击-->删除 5.接下来将图例靠上,选中图例,右击-->设置图例格式-->图例选项…

STM32——感应开关盖垃圾桶

STM32——感应开关盖垃圾桶 1.定时器介绍 软件定时 缺点&#xff1a;不精确、占用CPU资源 void Delay500ms() //11.0592MHz {unsigned char i, j, k;_nop_();i 4;j 129;k 119;do{do{while (--k);} while (--j);} while (--i); }定时器工作原理 使用精准的时基&#xff…

【Tomcat与网络8】从源码看Tomcat的层次结构

在前面我们介绍了如何通过源码来启动Tomcat&#xff0c;本文我们就来看一下Tomcat是如何一步步启动的&#xff0c;以及在启动过程中&#xff0c;不同的组件是如何加载的。 一般&#xff0c;我们可以通过 Tomcat 的 /bin 目录下的脚本 startup.sh 来启动 Tomcat&#xff0c;如果…

如何用MapTalks IDE来发布网站?

简介 MapTalks IDE 全称 MapTalks集成设计环境&#xff08;Integrated Design Environment&#xff09;&#xff0c;是由MapTalks技术团队开发的新一代web地图设计软件。 通过MapTalks IDE&#xff0c;您可以自由的创建二维和三维地图&#xff0c;在其中载入或创建地理数据&a…

计算机语言的发展历史

计算机编程语言的发展&#xff0c;是随着计算机本身硬件发展而发展的。硬件速度越快、体积越小、成本越低&#xff0c;应用到人类社会的场景就会越多&#xff0c;那么所需要的算法就会越复杂&#xff0c;也就要求计算机编程语言越高级。最初重达几十吨但一秒只能运算5000次的EN…