併發衝突問題
, 是平常開發中一個比較常見的問題。前端
不一樣用戶在較短期間隔內變動數據,或者某一個用戶進行的重複提交操做均可能致使併發衝突。ios
併發場景在開發和測試階段難以排查全面,出現線上 bug 之後定位困難,所以作好併發控制是先後端開發過程當中都須要重視的問題。npm
對於同一用戶短期內重複提交數據的問題,前端一般能夠先作一層攔截。axios
本文將討論前端如何利用 axios 的攔截器過濾重複請求,解決併發衝突。後端
在嘗試 axios 攔截器以前,先看看咱們以前業務是怎麼處理併發衝突問題的:數組
每次用戶操做頁面上的控件(輸入框、按鈕等),向後端發送請求的時候,都給頁面對應的控件添加 loading 效果,提示正在進行數據加載,同時也阻止 loading 效果結束前用戶繼續操做控件。promise
這是最直接有效的方式,若是大家前端團隊成員足夠細心耐心,擁有良好的編碼習慣,這樣就能夠解決大部分用戶不當心重複提交帶來的併發問題了。緩存
項目中須要前端限制併發的場景這麼多,咱們固然要思考更優更省事的方案。網絡
既然是在每次發送請求的時候進行併發控制,那若是能從新封裝下發請求的公共函數,統一處理重複請求實現自動攔截,就能夠大大簡化咱們的業務代碼。併發
項目使用的 axios
庫來發送 http
請求,axios
官方爲咱們提供了豐富的 API,咱們來看看攔截請求須要用到的兩個核心 API:
interceptors
攔截器包括請求攔截器和響應攔截器,能夠在請求發送前或者響應後進行攔截處理,用法以下:
// 添加請求攔截器 axios.interceptors.request.use(function (config) { // 在發送請求以前作些什麼 return config; }, function (error) { // 對請求錯誤作些什麼 return Promise.reject(error); }); // 添加響應攔截器 axios.interceptors.response.use(function (response) { // 對響應數據作點什麼 return response; }, function (error) { // 對響應錯誤作點什麼 return Promise.reject(error); });
cancel token
:調用 cancel token API
能夠取消請求。官網提供了兩種方式來構建 cancel token
,咱們採用這種方式:經過傳遞一個 executor
函數到 CancelToken
的構造函數來建立 cancel token
,方便在上面的請求攔截器中檢測到重複請求能夠當即執行:
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { // executor 函數接收一個 cancel 函數做爲參數 cancel = c; }) }); // cancel the request cancel();
本文提供的思路就是利用 axios interceptors API
攔截請求,檢測是否有多個相同的請求同時處於 pending 狀態,若是有就調用 cancel token API
取消重複的請求。
假如用戶重複點擊按鈕,前後提交了 A 和 B 這兩個徹底相同(考慮請求路徑、方法、參數)的請求,咱們能夠從如下幾種攔截方案中選擇其一:
第三種方案須要作監聽處理增長了複雜性,結合咱們實際的業務需求,最後採用了第二種方案來實現,即:
只發第一個請求。在 A 請求還處於 pending 狀態時,後發的全部與 A 重複的請求都取消,實際只發出 A 請求,直到 A 請求結束(成功/失敗)才中止對這個請求的攔截。
首先咱們要將項目中全部的 pending 狀態的請求存儲在一個變量中,叫它 pendingRequests
,
能夠經過把 axios
封裝爲一個單例模式的類,或者定義全局變量,來保證 pendingRequests
變量在每次發送請求前均可以訪問,並檢查是否爲重複的請求。
let pendingRequests = new Map()
把每一個請求的方法、url 和參數組合成一個字符串,做爲標識該請求的惟一 key,同時也是 pendingRequests
對象的 key:
const requestKey = `${config.url}/${JSON.stringify(config.params)}/${JSON.stringify(config.data)}&request_type=${config.method}`;
幫助理解的小 tips:
pendingRequests
爲 map 對象的目的是爲了方便咱們查詢它是否包含某個 key,以及添加和刪除 key。添加 key 時,對應的 value 能夠設置用戶自定義的一些功能參數,後面擴展功能的時候會用到。config
是 axios
攔截器中的參數,包含當前請求的信息在請求發出前檢查當前請求是否重複
在請求攔截器中,生成上面的 requestKey
,檢查 pendingRequests
對象中是否包含當前請求的 requestKey
requestKey
添加到 pendingRequests
對象中由於後面的響應攔截器中還要用到當前請求的 requestKey
,爲了不踩坑,最好不要再次生成,在這一步就把 requestKey
存回 axios
攔截器的 config
參數中,後面能夠直接在響應攔截器中經過 response.config.requestKey
取到。
代碼示例:
// 請求攔截器 axios.interceptors.request.use( (config) => { if (pendingRequests.has(requestKey)) { config.cancelToken = new axios.CancelToken((cancel) => { // cancel 函數的參數會做爲 promise 的 error 被捕獲 cancel(`重複的請求被主動攔截: ${requestKey}`); }); } else { pendingRequests.set(requestKey, config); config.requestKey = requestKey; } return config; }, (error) => { // 這裏出現錯誤多是網絡波動形成的,清空 pendingRequests 對象 pendingRequests.clear(); return Promise.reject(error); } );
pendingRequests
對象若是請求順利走到了響應攔截器這一步,說明這個請求已經結束了 pending 狀態,那咱們要把它從 pendingRequests
中除名:
axios.interceptors.response.use((response) => { const requestKey = response.config.requestKey; pendingRequests.delete(requestKey); return Promise.resolve(response); }, (error) => { if (axios.isCancel(error)) { console.warn(error); return Promise.reject(error); } pendingRequests.clear(); return Promise.reject(error); })
pendingRequests
對象的場景遇到網絡波動或者超時等狀況形成請求錯誤時,須要清空原來存儲的全部 pending 狀態的請求記錄,在上面演示的代碼已經做了註釋說明。
此外,頁面切換時也須要清空以前緩存的 pendingRequests
對象,能夠利用 Vue Router
的 beforeEach
鉤子:
router.beforeEach((to, from, next) => { request.clearRequestList(); next(); });
與後端約定好接口返回數據的格式,對接口報錯的狀況,能夠統一在響應攔截器中添加 toast 給用戶提示,
對於特殊的不須要報錯的接口,能夠設置一個參數存入 axios
攔截器的 config
參數中,過濾掉報錯提示:
// 接口返回 retcode 不爲 0 時須要報錯,請求設置了 noError 爲 true 則這個接口不報錯 if ( response.data.retcode && !response.config.noError ) { if (response.data.message) { Vue.prototype.$message({ showClose: true, message: response.data.message, type: 'error', }); } return Promise.reject(response.data); }
上面利用 axios interceptors
過濾重複請求時,能夠在控制檯拋出信息給開發者提示,在這個基礎上若是能給頁面上操做的控件添加 loading 效果就會對用戶更友好。
常見的 ui 組件庫都有提供 loading 服務,能夠指定頁面上須要添加 loading 效果的控件。下面是以 element UI
爲例的示例代碼:
// 給 loadingTarget 對應的控件添加 loading 效果,儲存 loadingService 實例 addLoading(config) { if (!document.querySelector(config.loadingTarget)) return; config.loadingService = Loading.service({ target: config.loadingTarget, }); } // 調用 loadingService 實例的 close 方法關閉對應元素的 loading 效果 closeLoading(config) { config.loadingService && config.loadingService.close(); }
與上面過濾報錯方式相似,發請求的時候將元素的 class name 或 id 存入 axios
攔截器的 config
參數中,
在請求攔截器中調用 addLoading
方法, 響應攔截器中調用 closeLoading
方法,就能夠實如今請求 pending 過程當中指定控件(如 button) loading,請求結束後控件自動取消 loading 效果。
簡單看下 axios interceptors
部分實現源碼能夠理解,它支持定義多個 interceptors
,因此只要咱們定義的 interceptors
符合 Promise.then
鏈式調用的規範,還能夠添加更多功能:
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { chain.unshift(interceptor.fulfilled, interceptor.rejected); }); this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { chain.push(interceptor.fulfilled, interceptor.rejected); }); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
併發問題很常見,處理起來又相對繁瑣,前端解決併發衝突時,能夠利用 axios
攔截器統一處理重複請求,簡化業務代碼。
同時 axios
攔截器支持更多應用,本文提供了部分經常使用擴展功能的實現,感興趣的同窗能夠繼續挖掘補充攔截器的其餘用法。
今天的內容就這麼多,但願對你有幫助。
若是以爲內容有幫助, 能夠關注下個人公衆號,掌握最新動態,一塊兒學習!