欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

前端接口防止重復(fù)請(qǐng)求實(shí)現(xiàn)方案

admin
2024年12月23日 12:40 本文熱度 112

?


前言

前段時(shí)間老板心血來(lái)潮,要我們前端組對(duì)整個(gè)的項(xiàng)目都做一下接口防止重復(fù)請(qǐng)求的處理(似乎是有用戶通過一些快速點(diǎn)擊薅到了一些優(yōu)惠券啥的)。。。聽到這個(gè)需求,第一反應(yīng)就是,防止薅羊毛最保險(xiǎn)的方案不還是在服務(wù)端加限制嗎?前端加限制能夠攔截的畢竟有限。可老板就是執(zhí)意要前端搞一下子,行吧,搞就搞吧。

雖然大部分的接口處理我們都是加了loading的,但又不能確保真的是每個(gè)接口都加了的,可是如果要一個(gè)接口一個(gè)接口的排查,那這維護(hù)了四五年的系統(tǒng),成百上千的接口肯定要耗費(fèi)非常多的精力,根本就是不現(xiàn)實(shí)的,所以就只能去做全局處理

現(xiàn)在,我們就來(lái)總結(jié)一下這次的防重復(fù)請(qǐng)求的實(shí)現(xiàn)方案:

方案一

這個(gè)方案是最容易想到也是最樸實(shí)無(wú)華的一個(gè)方案:通過使用axios攔截器,在請(qǐng)求攔截器中開啟全屏Loading,然后在響應(yīng)攔截器中將Loading關(guān)閉。


這個(gè)方案固然已經(jīng)可以滿足我們目前的需求,但不管三七二十一,直接搞個(gè)全屏Loading還是不太美觀,何況在目前項(xiàng)目的接口處理邏輯中還有一些局部Loading,就有可能會(huì)出現(xiàn)Loading套Loading的情況,兩個(gè)圈一起轉(zhuǎn),頭皮發(fā)麻。

方案二

加Loading的方案不太友好,而對(duì)于同一個(gè)接口,如果傳參都是一樣的,一般來(lái)說(shuō)都沒有必要連續(xù)請(qǐng)求多次吧。那我們可不可以通過代碼邏輯直接把完全相同的請(qǐng)求給攔截掉,不讓它到達(dá)服務(wù)端呢?這個(gè)思路不錯(cuò),我們說(shuō)干就干。

首先,我們要判斷什么樣的請(qǐng)求屬于是相同請(qǐng)求:

一個(gè)請(qǐng)求包含的內(nèi)容不外乎就是請(qǐng)求方法,地址,參數(shù)以及請(qǐng)求發(fā)出的頁(yè)面hash。那我們是不是就可以根據(jù)這幾個(gè)數(shù)據(jù)把這個(gè)請(qǐng)求生成一個(gè)key來(lái)作為這個(gè)請(qǐng)求的標(biāo)識(shí)呢?

// 根據(jù)請(qǐng)求生成對(duì)應(yīng)的keyfunction generateReqKey(config, hash) {    const { method, url, params, data } = config;    return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");}

有了請(qǐng)求的key,我們就可以在請(qǐng)求攔截器中把每次發(fā)起的請(qǐng)求給收集起來(lái),后續(xù)如果有相同請(qǐng)求進(jìn)來(lái),那都去這個(gè)集合中去比對(duì),如果已經(jīng)存在了,說(shuō)明就是一個(gè)重復(fù)的請(qǐng)求,我們就給攔截掉。

當(dāng)請(qǐng)求完成響應(yīng)后,再將這個(gè)請(qǐng)求從集合中移除。合理,nice!

具體實(shí)現(xiàn)如下:


是不是覺得這種方案還不錯(cuò),萬(wàn)事大吉?

no,no,no! 這個(gè)方案雖然理論上是解決了接口防重復(fù)請(qǐng)求這個(gè)問題,但是它會(huì)引發(fā)更多的問題。

比如,我有這樣一個(gè)接口處理:


那么,當(dāng)我們觸發(fā)多次請(qǐng)求時(shí):


這里我連續(xù)點(diǎn)擊了4次按鈕,可以看到,的確是只有一個(gè)請(qǐng)求發(fā)送出去,可是因?yàn)樵诖a邏輯中,我們對(duì)錯(cuò)誤進(jìn)行了一些處理,所以就將報(bào)錯(cuò)消息提示了3次,這樣是很不友好的,而且,如果在錯(cuò)誤捕獲中有做更多的邏輯處理,那么很有可能會(huì)導(dǎo)致整個(gè)程序的異常。

而且,這種方案還會(huì)有另外一個(gè)比較嚴(yán)重的問題:

我們?cè)谏厦嬖谏烧?qǐng)求key的時(shí)候把hash考慮進(jìn)去了(如果是history路由,可以將pathname加入生成key),這是因?yàn)轫?xiàng)目中會(huì)有一些數(shù)據(jù)字典型的接口,這些接口可能有不同頁(yè)面都需要去調(diào)用,如果第一個(gè)頁(yè)面請(qǐng)求的字典接口比較慢,第二個(gè)頁(yè)面的接口就被攔截了,最后就會(huì)導(dǎo)致第二個(gè)頁(yè)面邏輯錯(cuò)誤。

那么這么一看,我們生成key的時(shí)候加入了hash,講道理就沒問題了呀。

可是倘若我這兩個(gè)請(qǐng)求是來(lái)自同一個(gè)頁(yè)面呢?

比如,一個(gè)頁(yè)面同時(shí)加載兩個(gè)組件,而這兩個(gè)組件都需要調(diào)用某個(gè)接口時(shí):


那么此時(shí),后調(diào)接口的組件就無(wú)法拿到正確數(shù)據(jù)了。啊?這,真是難頂!

方案三

方案二的路子,我們發(fā)現(xiàn)確實(shí)問題重重,那么接下來(lái)我們來(lái)看第三種方案,也是我們最終采用的方案。

延續(xù)我們方案二的前面思路,仍然是攔截相同請(qǐng)求,但這次我們可不可以不直接把請(qǐng)求掛掉,而是對(duì)于相同的請(qǐng)求我們先給它掛起,等到最先發(fā)出去的請(qǐng)求拿到結(jié)果回來(lái)之后,把成功或失敗的結(jié)果共享給后面到來(lái)的相同請(qǐng)求。


思路我們已經(jīng)明確了,但這里有幾個(gè)需要注意的點(diǎn):

  • 我們?cè)谀玫巾憫?yīng)結(jié)果后,返回給之前我們掛起的請(qǐng)求時(shí),我們要用到發(fā)布訂閱模式(日常在面試題中看到,這次終于讓我給用上了(^▽^))
  • 對(duì)于掛起的請(qǐng)求,我們需要將它攔截,不能讓它執(zhí)行正常的請(qǐng)求邏輯,所以一定要在請(qǐng)求攔截器中通過return Promise.reject()來(lái)直接中斷請(qǐng)求,并做一些特殊的標(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ù)請(qǐng)求生成對(duì)應(yīng)的keyfunction generateReqKey(config, hash) {    const { method, url, params, data } = config;    return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");}
// 存儲(chǔ)已發(fā)送但未響應(yīng)的請(qǐng)求const pendingRequest = new Set();// 發(fā)布訂閱容器const ev = new EventEmitter()
// 添加請(qǐng)求攔截器instance.interceptors.request.use(async (config) => {    let hash = location.hash    // 生成請(qǐng)求Key    let reqKey = generateReqKey(config, hash)
   if(pendingRequest.has(reqKey)) {        // 如果是相同請(qǐng)求,在這里將請(qǐng)求掛起,通過發(fā)布訂閱來(lái)為該請(qǐng)求返回結(jié)果        // 這里需注意,拿到結(jié)果后,無(wú)論成功與否,都需要return Promise.reject()來(lái)中斷這次請(qǐng)求,否則請(qǐng)求會(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{        // 將請(qǐng)求的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ǔ)充

到這里,這么一通操作下來(lái)上面的代碼講道理是萬(wàn)無(wú)一失了,但不得不說(shuō),線上的情況仍然是復(fù)雜多樣的。而其中一個(gè)比較特殊的情況就是文件上傳。


可以看到,我在這里是上傳了兩個(gè)不同的文件的,但只調(diào)用了一次上傳接口。按理說(shuō)是兩個(gè)不同的請(qǐng)求,可為什么會(huì)被我們前面寫的邏輯給攔截掉一個(gè)呢?

我們打印一下請(qǐng)求的config:


可以看到,請(qǐng)求體data中的數(shù)據(jù)是FormData類型,而我們?cè)谏烧?qǐng)求key的時(shí)候,是通過JSON.stringify方法進(jìn)行操作的,而對(duì)于FormData類型的數(shù)據(jù)執(zhí)行該函數(shù)得到的只有{}。

所以,對(duì)于文件上傳,盡管我們上傳了不同的文件,但它們所發(fā)出的請(qǐng)求生成的key都是一樣的,這么一來(lái)就觸發(fā)了我們前面的攔截機(jī)制。

那么我們接下來(lái)我們只需要在我們?cè)瓉?lái)的攔截邏輯中判斷一下請(qǐng)求體的數(shù)據(jù)類型即可,如果含有FormData類型的數(shù)據(jù),我們就直接放行不再關(guān)注這個(gè)請(qǐng)求就是了。

function isFileUploadApi(config) {  return Object.prototype.toString.call(config.data) === "[object FormData]"}

最后

到這里,整個(gè)的需求總算是完結(jié)啦!不用一個(gè)個(gè)接口的改代碼,又可以愉快的打代碼了,nice!

Demo地址:https://github.com/GuJiugc/JueJinDemo

本文完~


閱讀原文:https://mp.weixin.qq.com/s/qrsYhY9Qqc4XtqorFgjErg


該文章在 2024/12/24 9:48:29 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國(guó)內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場(chǎng)、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場(chǎng)作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉(cāng)儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購(gòu)管理,倉(cāng)儲(chǔ)管理,倉(cāng)庫(kù)管理,保質(zhì)期管理,貨位管理,庫(kù)位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved