Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

大厂技术  高级前端  Node 进阶

点击上方 程序员成长指北,关注公众号

回复 1,加入高级 Node 交流群

缓存池不过就是一个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) })

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

问题

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

思考

有了!我们写个定时器就好了呀,比如我们可以给第二次请求加个定时器,定时器时间到了再去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) }) image.pngimage.png

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

优化🤔

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

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

// 定义一下回调的格式 interface RequestCallback {   onSuccess: (data: any) => void   onError: (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) }) image.pngimage.png

OK,完成了。

再试一下失败的时候

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)     } ) image.png

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) => void   onError: (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)     }   ) }

最后

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

关于本文

作者:背对疾风

https://juejin.cn/post/7104635370796482567

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

   “分享、点赞、在看” 支持一下