JS前端接口防止重復(fù)請求的三種實(shí)現(xiàn)方案
前言
前段時(shí)間老板心血來潮,要我們前端組對整個(gè)的項(xiàng)目都做一下接口防止重復(fù)請求的處理(似乎是有用戶通過一些快速點(diǎn)擊薅到了一些優(yōu)惠券啥的)。。。聽到這個(gè)需求,第一反應(yīng)就是,防止薅羊毛最保險(xiǎn)的方案不還是在服務(wù)端加限制嗎?前端加限制能夠攔截的畢竟有限??衫习寰褪菆?zhí)意要前端搞一下子,行吧,搞就搞吧,you happy jiu ok。
雖然大部分的接口處理我們都是加了loading的,但又不能確保真的是每個(gè)接口都加了的,可是如果要一個(gè)接口一個(gè)接口的排查,那這維護(hù)了四五年的系統(tǒng),成百上千的接口肯定要耗費(fèi)非常多的精力,根本就是不現(xiàn)實(shí)的,所以就只能去做全局處理。下面就來總結(jié)一下這次的防重復(fù)請求的實(shí)現(xiàn)方案:
方案一
這個(gè)方案是最容易想到也是最樸實(shí)無華的一個(gè)方案:通過使用axios攔截器,在請求攔截器中開啟全屏Loading,然后在響應(yīng)攔截器中將Loading關(guān)閉。
這個(gè)方案固然已經(jīng)可以滿足我們目前的需求,但不管三七二十一,直接搞個(gè)全屏Loading還是不太美觀,何況在目前項(xiàng)目的接口處理邏輯中還有一些局部Loading,就有可能會(huì)出現(xiàn)Loading套Loading的情況,兩個(gè)圈一起轉(zhuǎn),頭皮發(fā)麻。
方案二
加Loading的方案不太友好,而對于同一個(gè)接口,如果傳參都是一樣的,一般來說都沒有必要連續(xù)請求多次吧。那我們可不可以通過代碼邏輯直接把完全相同的請求給攔截掉,不讓它到達(dá)服務(wù)端呢?這個(gè)思路不錯(cuò),我們說干就干。
首先,我們要判斷什么樣的請求屬于是相同請求:
一個(gè)請求包含的內(nèi)容不外乎就是請求方法,地址,參數(shù)以及請求發(fā)出的頁面hash。那我們是不是就可以根據(jù)這幾個(gè)數(shù)據(jù)把這個(gè)請求生成一個(gè)key來作為這個(gè)請求的標(biāo)識呢?
// 根據(jù)請求生成對應(yīng)的key function generateReqKey(config, hash) { const { method, url, params, data } = config; return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&"); }
有了請求的key,我們就可以在請求攔截器中把每次發(fā)起的請求給收集起來,后續(xù)如果有相同請求進(jìn)來,那都去這個(gè)集合中去比對,如果已經(jīng)存在了,說明就是一個(gè)重復(fù)的請求,我們就給攔截掉。當(dāng)請求完成響應(yīng)后,再將這個(gè)請求從集合中移除。合理,nice!
具體實(shí)現(xiàn)如下:
是不是覺得這種方案還不錯(cuò),萬事大吉?
no,no,no! 這個(gè)方案雖然理論上是解決了接口防重復(fù)請求這個(gè)問題,但是它會(huì)引發(fā)更多的問題。
比如,我有這樣一個(gè)接口處理:
那么,當(dāng)我們觸發(fā)多次請求時(shí):
這里我連續(xù)點(diǎn)擊了4次按鈕,可以看到,的確是只有一個(gè)請求發(fā)送出去,可是因?yàn)樵诖a邏輯中,我們對錯(cuò)誤進(jìn)行了一些處理,所以就將報(bào)錯(cuò)消息提示了3次,這樣是很不友好的,而且,如果在錯(cuò)誤捕獲中有做更多的邏輯處理,那么很有可能會(huì)導(dǎo)致整個(gè)程序的異常。
而且,這種方案還會(huì)有另外一個(gè)比較嚴(yán)重的問題:
我們在上面在生成請求key的時(shí)候把hash考慮進(jìn)去了(如果是history路由,可以將pathname加入生成key),這是因?yàn)轫?xiàng)目中會(huì)有一些數(shù)據(jù)字典型的接口,這些接口可能有不同頁面都需要去調(diào)用,如果第一個(gè)頁面請求的字典接口比較慢,第二個(gè)頁面的接口就被攔截了,最后就會(huì)導(dǎo)致第二個(gè)頁面邏輯錯(cuò)誤。那么這么一看,我們生成key的時(shí)候加入了hash,講道理就沒問題了呀。
可是倘若我這兩個(gè)請求是來自同一個(gè)頁面呢?
比如,一個(gè)頁面同時(shí)加載兩個(gè)組件,而這兩個(gè)組件都需要調(diào)用某個(gè)接口時(shí):
那么此時(shí),后調(diào)接口的組件就無法拿到正確數(shù)據(jù)了。啊這,真是難頂!
方案三
方案二的路子,我們發(fā)現(xiàn)確實(shí)問題重重,那么接下來我們來看第三種方案,也是我們最終采用的方案。
延續(xù)我們方案二的前面思路,仍然是攔截相同請求,但這次我們可不可以不直接把請求掛掉,而是對于相同的請求我們先給它掛起,等到最先發(fā)出去的請求拿到結(jié)果回來之后,把成功或失敗的結(jié)果共享給后面到來的相同請求。
思路我們已經(jīng)明確了,但這里有幾個(gè)需要注意的點(diǎn):
- 我們在拿到響應(yīng)結(jié)果后,返回給之前我們掛起的請求時(shí),我們要用到發(fā)布訂閱模式(日常在面試題中看到,這次終于讓我給用上了(^▽^))
- 對于掛起的請求,我們需要將它攔截,不能讓它執(zhí)行正常的請求邏輯,所以一定要在請求攔截器中通過
return Promise.reject()
來直接中斷請求,并做一些特殊的標(biāo)記,以便于在響應(yīng)攔截器中進(jìn)行特殊處理。
最后,直接附上完整代碼:
import axios from "axios" let instance = axios.create({ baseURL: "/api/" }) // 發(fā)布訂閱 class EventEmitter { constructor() { this.event = {} } on(type, cbres, cbrej) { if (!this.event[type]) { this.event[type] = [[cbres, cbrej]] } else { this.event[type].push([cbres, cbrej]) } } emit(type, res, ansType) { if (!this.event[type]) return else { this.event[type].forEach(cbArr => { if(ansType === 'resolve') { cbArr[0](res) }else{ cbArr[1](res) } }); } } } // 根據(jù)請求生成對應(yīng)的key function generateReqKey(config, hash) { const { method, url, params, data } = config; return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&"); } // 存儲(chǔ)已發(fā)送但未響應(yīng)的請求 const pendingRequest = new Set(); // 發(fā)布訂閱容器 const ev = new EventEmitter() // 添加請求攔截器 instance.interceptors.request.use(async (config) => { let hash = location.hash // 生成請求Key let reqKey = generateReqKey(config, hash) if(pendingRequest.has(reqKey)) { // 如果是相同請求,在這里將請求掛起,通過發(fā)布訂閱來為該請求返回結(jié)果 // 這里需注意,拿到結(jié)果后,無論成功與否,都需要return Promise.reject()來中斷這次請求,否則請求會(huì)正常發(fā)送至服務(wù)器 let res = null try { // 接口成功響應(yīng) res = await new Promise((resolve, reject) => { ev.on(reqKey, resolve, reject) }) return Promise.reject({ type: 'limiteResSuccess', val: res }) }catch(limitFunErr) { // 接口報(bào)錯(cuò) return Promise.reject({ type: 'limiteResError', val: limitFunErr }) } }else{ // 將請求的key保存在config config.pendKey = reqKey pendingRequest.add(reqKey) } return config; }, function (error) { return Promise.reject(error); }); // 添加響應(yīng)攔截器 instance.interceptors.response.use(function (response) { // 將拿到的結(jié)果發(fā)布給其他相同的接口 handleSuccessResponse_limit(response) return response; }, function (error) { return handleErrorResponse_limit(error) }); // 接口響應(yīng)成功 function handleSuccessResponse_limit(response) { const reqKey = response.config.pendKey if(pendingRequest.has(reqKey)) { let x = null try { x = JSON.parse(JSON.stringify(response)) }catch(e) { x = response } pendingRequest.delete(reqKey) ev.emit(reqKey, x, 'resolve') delete ev.reqKey } } // 接口走失敗響應(yīng) function handleErrorResponse_limit(error) { if(error.type && error.type === 'limiteResSuccess') { return Promise.resolve(error.val) }else if(error.type && error.type === 'limiteResError') { return Promise.reject(error.val); }else{ const reqKey = error.config.pendKey if(pendingRequest.has(reqKey)) { let x = null try { x = JSON.parse(JSON.stringify(error)) }catch(e) { x = error } pendingRequest.delete(reqKey) ev.emit(reqKey, x, 'reject') delete ev.reqKey } } return Promise.reject(error); } export default instance;
補(bǔ)充
到這里,這么一通操作下來上面的代碼講道理是萬無一失了,但不得不說,線上的情況仍然是復(fù)雜多樣的。而其中一個(gè)比較特殊的情況就是文件上傳。
可以看到,我在這里是上傳了兩個(gè)不同的文件的,但只調(diào)用了一次上傳接口。按理說是兩個(gè)不同的請求,可為什么會(huì)被我們前面寫的邏輯給攔截掉一個(gè)呢?
我們打印一下請求的config:
可以看到,請求體data中的數(shù)據(jù)是FormData類型,而我們在生成請求key的時(shí)候,是通過JSON.stringify
方法進(jìn)行操作的,而對于FormData類型的數(shù)據(jù)執(zhí)行該函數(shù)得到的只有{}
。所以,對于文件上傳,盡管我們上傳了不同的文件,但它們所發(fā)出的請求生成的key都是一樣的,這么一來就觸發(fā)了我們前面的攔截機(jī)制。
那么我們接下來我們只需要在我們原來的攔截邏輯中判斷一下請求體的數(shù)據(jù)類型即可,如果含有FormData類型的數(shù)據(jù),我們就直接放行不再關(guān)注這個(gè)請求就是了。
function isFileUploadApi(config) { return Object.prototype.toString.call(config.data) === "[object FormData]" }
最后
到這里,整個(gè)的需求總算是完結(jié)啦!不用一個(gè)個(gè)接口的改代碼,又可以愉快的打代碼了,nice!
以上就是JS前端接口防止重復(fù)請求的三種實(shí)現(xiàn)方案的詳細(xì)內(nèi)容,更多關(guān)于JS前端接口防止重復(fù)請求的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中使用正則匹配多條,且獲取每條中的分組數(shù)據(jù)
該問題在使用Ajax遠(yuǎn)程獲取某網(wǎng)頁數(shù)據(jù)時(shí)經(jīng)常遇見 如果目標(biāo)頁面是XML,就好辦了,實(shí)用XMLDOM可以很輕松完成任務(wù)。2010-11-11用xhtml+css寫的相冊自適應(yīng) - 類似九宮格[兼容 ff ie6 ie7 opear ]
用xhtml+css寫的相冊自適應(yīng) - 類似九宮格[兼容 ff ie6 ie7 opear ]...2007-05-05layer.open屬性詳解及l(fā)ayer.open彈出框使用post方法舉例
這篇文章主要給大家介紹了關(guān)于layer.open屬性詳解及l(fā)ayer.open彈出框使用post方法的相關(guān)資料,最近接觸到layer彈窗,感覺彈窗功能異常強(qiáng)大,真的很方便,所以記錄下來,需要的朋友可以參考下2023-12-12uniapp基礎(chǔ)篇之上傳圖片的實(shí)戰(zhàn)步驟
應(yīng)用uni-app開發(fā)跨平臺App項(xiàng)目時(shí),上傳圖片、文檔等資源功能需求十分常見,下面這篇文章主要給大家介紹了關(guān)于uniapp基礎(chǔ)篇之上傳圖片的相關(guān)資料,需要的朋友可以參考下2022-12-12