如何設計一個支持并發(fā)的前端緩存接口?

緩存池
緩存池不過就是一個map,存儲接口數(shù)據(jù)的地方,將接口的路徑和參數(shù)拼到一塊作為key,數(shù)據(jù)作為value存起來罷了,這個咱誰都會。
const?cacheMap?=?new?Map();
封裝一下調(diào)用接口的方法,調(diào)用時先走咱們緩存數(shù)據(jù)。
import?axios,?{?AxiosRequestConfig?}?from?'axios'
//?先來一個簡簡單單的發(fā)送
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?方式穿的參數(shù),我們的緩存一般都是?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
??})
}
然后調(diào)用
const?getArticleList?=?(params:?any)?=>
??sendRequest({
????needCache:?true,
????url:?'/article/list',
????method:?'get',
????params
??})
getArticleList({
??page:?1,
??pageSize:?10
}).then((res)?=>?{
??console.log(res)
})
這個部分就很簡單,我們在調(diào)接口時給一個needCache的標記,然后調(diào)完接口如果成功的話,就會將數(shù)據(jù)放到cacheMap中去,下次再調(diào)用的話,就直接返回緩存中的數(shù)據(jù)。
并發(fā)緩存
上面的雖然看似實現(xiàn)了緩存,不管我們調(diào)用幾次,都只會發(fā)送一次請求,剩下的都會走緩存。但是真的是這樣嗎?
getArticleList({
??page:?1,
??pageSize:?10
}).then((res)?=>?{
??console.log(res)
})
getArticleList({
??page:?1,
??pageSize:?10
}).then((res)?=>?{
??console.log(res)
})
其實這樣,就可以測出,我們的雖然設計了緩存,但是請求還是發(fā)送了兩次,這是因為我們第二次請求發(fā)出時,第一次請求還沒完成,也就沒給緩存池里放數(shù)據(jù),所以第二次請求沒命中緩存,也就又發(fā)了一次。
問題
那么,有沒有一種辦法讓第二次請求等待第一次請求調(diào)用完成,然后再一塊返回呢?
思考
有了!我們寫個定時器就好了呀,比如我們可以給第二次請求加個定時器,定時器時間到了再去cacheMap中查一遍有沒有緩存數(shù)據(jù),沒有的話可能是第一個請求還沒好,再等幾秒試試!
可是這樣的話,第一個請求的時候也會在原地等呀!??
那這樣的話,讓第一個請求在一個地方貼個告示不就好了,就像上廁所的時候在門口掛個牌子一樣!??
//?存儲緩存當前狀態(tài),相當于掛牌子的地方
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)
??????//?判斷當前的接口緩存狀態(tài),如果是?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.png
image.png成了!這里真的做到了,可以看到我們這里打印了兩次,但是只發(fā)了一次請求。
優(yōu)化??
可是用setTimeout等待還是不太優(yōu)雅,如果第一個請求能在3s以內(nèi)完成還行,用戶等待的時間還不算太久,還能忍受??扇绻?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">3.1s的話,第二個接口用戶可就白白等了6s之久,那么,有沒有一種辦法,能讓第一個接口完成后,接著就通知第二個接口返回數(shù)據(jù)呢?
等待,通知,這種場景我們寫代碼用的最多的就是回調(diào)了,但是這次用的是promise啊,而且還是毫不相干的兩個promise。等等!callback和promise,promise本身就是callback實現(xiàn)的!promise的then會在resole被調(diào)用時調(diào)用,這樣的話,我們可以將第二個請求的resole放在一個callback里,然后在第一個請求完成的時候,調(diào)用這個callback!??
//?定義一下回調(diào)的格式
interface?RequestCallback?{
??onSuccess:?(data:?any)?=>?void
??onError:?(error:?any)?=>?void
}
//?存放等待狀態(tài)的請求回調(diào)
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)
??????//?判斷當前的接口緩存狀態(tài),如果是?complete?,則代表緩存完成
??????if?(currentStatus?===?'complete')?{
????????return?Promise.resolve(cacheMap.get(cacheKey))
??????}
??????//?如果是?pending?,則代表正在請求中,這里放入回調(diào)函數(shù)
??????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?中的狀態(tài),能讓下次請求重新請求
????????statusMap.delete(cacheKey)
??????}
??????//?這里觸發(fā)resolve的回調(diào)函數(shù)
??????if?(callbackMap.has(cacheKey))?{
????????callbackMap.get(cacheKey)!.forEach((callback)?=>?{
??????????callback.onSuccess(res)
????????})
????????//?調(diào)用完成之后清掉,用不到了
????????callbackMap.delete(cacheKey)
??????}
??????return?res
????},
????(error)?=>?{
??????//?不成功的情況下刪掉?statusMap?中的狀態(tài),能讓下次請求重新請求
??????statusMap.delete(cacheKey)
??????//?這里觸發(fā)reject的回調(diào)函數(shù)
??????if?(callbackMap.has(cacheKey))?{
????????callbackMap.get(cacheKey)!.forEach((callback)?=>?{
??????????callback.onError(error)
????????})
????????//?調(diào)用完成之后也清掉
????????callbackMap.delete(cacheKey)
??????}
??????//?這里要返回?Promise.reject(error),才能被catch捕捉到
??????return?Promise.reject(error)
????}
??)
}
在判斷到當前請求狀態(tài)是pending時,將promise的resole與reject放入回調(diào)隊列中,等待被觸發(fā)調(diào)用。然后在請求完成時,觸發(fā)對應的請求隊列。
試一下
getArticleList({
????page:?1,
????pageSize:?10
}).then((res)?=>?{
????console.log(res)
})
getArticleList({
????page:?1,
????pageSize:?10
}).then((res)?=>?{
????console.log(res)
})
image.png
image.pngOK,完成了。
再試一下失敗的時候
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.pngOK,兩個都失敗了。(但是這里的error2早于error1打印,你知道是啥原因嗎???)
總結(jié)
promise封裝并發(fā)緩存到這里就結(jié)束啦,不過看到這里你可能會覺著沒啥用處,但是其實這也是我碰到的一個需求才延申出來的,當時的場景是一個頁面里有好幾個下拉選擇框,選項都是接口提供的常量。但是只接口提供了一個接口返回這些常量,前端拿到以后自己再根據(jù)類型挑出來,所以這種情況我們肯定不能每個下拉框都去調(diào)一次接口,只能是寄托緩存機制了。
這種寫法,在另一種場景下也很好用,比如將需要用戶操作的流程封裝成promise。例如,A頁面點擊A按鈕,出現(xiàn)一個B彈窗,彈窗里有B按鈕,用戶點擊B按鈕之后關閉彈窗,再彈出C彈窗C按鈕,點擊C之后流程完成,這種情況就很適合將每個彈窗里的操作流程都封裝成一個promise,最外面的A頁面只需要連著調(diào)用這幾個promise就可以了,而不需要維護控制這幾個彈窗顯示隱藏的變量了。
放一下全部代碼
import?axios,?{?AxiosRequestConfig?}?from?'axios'
import?qs?from?'qs'
//?存儲緩存數(shù)據(jù)
const?cacheMap?=?new?Map()
//?存儲緩存當前狀態(tài)
const?statusMap?=?new?Map<string,?'pending'?|?'complete'>()
//?定義一下回調(diào)的格式
interface?RequestCallback?{
??onSuccess:?(data:?any)?=>?void
??onError:?(error:?any)?=>?void
}
//?存放等待狀態(tài)的請求回調(diào)
const?callbackMap?=?new?Map<string,?RequestCallback[]>()
interface?MyRequestConfig?extends?AxiosRequestConfig?{
??needCache?:?boolean
}
//?這里用params是因為params是?GET?方式穿的參數(shù),我們的緩存一般都是?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)
??????//?判斷當前的接口緩存狀態(tài),如果是?complete?,則代表緩存完成
??????if?(currentStatus?===?'complete')?{
????????return?Promise.resolve(cacheMap.get(cacheKey))
??????}
??????//?如果是?pending?,則代表正在請求中,這里放入回調(diào)函數(shù)
??????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?中的狀態(tài),能讓下次請求重新請求
????????statusMap.delete(cacheKey)
??????}
??????//?這里觸發(fā)resolve的回調(diào)函數(shù)
??????if?(callbackMap.has(cacheKey))?{
????????callbackMap.get(cacheKey)!.forEach((callback)?=>?{
??????????callback.onSuccess(res)
????????})
????????//?調(diào)用完成之后清掉,用不到了
????????callbackMap.delete(cacheKey)
??????}
??????return?res
????},
????(error)?=>?{
??????//?不成功的情況下刪掉?statusMap?中的狀態(tài),能讓下次請求重新請求
??????statusMap.delete(cacheKey)
??????//?這里觸發(fā)reject的回調(diào)函數(shù)
??????if?(callbackMap.has(cacheKey))?{
????????callbackMap.get(cacheKey)!.forEach((callback)?=>?{
??????????callback.onError(error)
????????})
????????//?調(diào)用完成之后也清掉
????????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)
????}
??)
}
最后
對請求結(jié)果是否成功那里處理的比較簡陋,項目里用到的話根據(jù)自己情況來。
關于本文
作者:背對疾風https://juejin.cn/post/7104635370796482567
