文章目录
- 一、概述
- 二、并发缓存
- 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)
})
成了!这里真的做到了,可以看到我们这里打印了两次,但是只发了一次请求。
2.3、优化
可是用setTimeout
等待还是不太优雅,如果第一个请求能在3s
以内完成还行,用户等待的时间还不算太久,还能忍受。可如果是3.1s
的话,第二个接口用户可就白白等了6s
之久,那么,有没有一种办法,能让第一个接口
完成后,接着就通知第二个接口
返回数据呢?
等待,通知,这种场景我们写代码用的最多的就是回调了,但是这次用的是promise
啊,而且还是毫不相干的两个promise
。等等!callback
和promise
,promise
本身就是callback
实现的!promise
的then
会在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
时,将promise
的resole
与reject
放入回调队列中,等待被触发调用。然后在请求完成时,触发对应的请求队列。
试一下
getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})
getArticleList({page: 1,pageSize: 10
}).then((res) => {console.log(res)
})
再试一下失败的时候
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)}
)
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)})
}
四、最后
本人每篇文章都是一字一句码出来,希望对大家有所帮助,多提提意见。顺手来个三连击,点赞👍收藏💖关注✨,一起加油☕