原文地址:https://monine.github.io/#/ar...vue
最近工做賊忙,這篇文章按說應該兩個月以前就產出,但是天天的精力基本都用在工做上,一寫文章就犯迷糊,斷斷續續的每次要從新屢邏輯,之後不再這樣了。這篇文章是我司後臺項目中遇到的一個基礎需求,本身設計了一個實現方案,感受還不錯。webpack
後端接口響應,根據與後端約定的狀態碼(非 http 狀態碼)斷定接口是否異常,我司的約定是 status !== 0
則表示接口異常。一旦接口處於異常狀態,先讓業務端(調用者)處理異常,再由業務端決定是否執行接口異常統一處理(目前我司的統一處理內容就是彈出個 element-ui message 提示消息 😂)。ios
start=>start: 接口響應 isApiError=>condition: 異常? normalProcess=>operation: 執行正常接口處理流程 isUserHandle=>condition: 業務端處理? userProcess=>operation: 執行業務端處理 isNeedUniteHandle=>condition: 統一處理? uniteProcess=>operation: 執行統一處理 end=>end: 結束 start->isApiError isApiError(no)->normalProcess->end isApiError(yes)->isUserHandle isUserHandle(no)->uniteProcess->end isUserHandle(yes)->userProcess->isNeedUniteHandle isNeedUniteHandle(yes)->uniteProcess->end isNeedUniteHandle(no)->end
這個流程有一個難點,當接口響應後處於異常狀態,先交由業務端處理,再由業務端決定是否執行統一處理?git
API 層我司使用的是第三方庫 axios,接口響應後會先走響應攔截器,再走業務端代碼。
正常的接口異常統一處理流程,是在響應攔截器內斷定,與後端約定的響應狀態碼是否爲異常狀態碼。若是是,則先執行統一處理邏輯,再交由到業務端處理。那如今的需求是將接口異常處理的流程逆轉,接口響應狀態異常以後,先交由業務端執行異常處理,再由業務端決定是否執行接口異常狀態統一處理。github
如上所說,若是接口處於異常狀態,須要斷定是否要執行統一處理,分兩種狀況:web
問題來了,接口異常統一處理的代碼應該寫在哪裏?如何保證它在狀態異常狀況下,先交由業務端處理,再根據業務端的聲明斷定是否執行統一處理。vue-cli
我司以前已經有過處理方案,不過是隻針對 vue 框架下的處理,經過 mixin 將 methods 內全部方法進行覆寫,補丁函數內對源函數執行完成以後獲取的返回結果進行斷定,若是返回結果爲 Promise 類型,則繼續進行相關異常處理操做。這樣確實可以達到實現效果,但總以爲很不優雅:element-ui
當時我瞭解到上述的處理方案後第一反應是 API 層的任何操做都不該該與框架自己進行任何關聯綁定,如同當年 vue 從全家桶中移除 vue-resource 同樣。axios
通過一些思考,大體肯定了一個思路:利用 Promise 狀態的穩定,以接口名稱做爲惟一標識,表示當前接口是否還須要執行統一處理。。後端
我是這樣設計的,接口調用時使用 url 做爲惟一標識,以狀態的形式保存在數組內 const unhandleAPI = []
。
接口返回後進入響應攔截器,在此對接口響應狀態進行判斷,若是屬於異常狀態,則使用 setTimeout
將接口異常統一處理函數設置爲 macro task,做爲一個異步任務推遲到下一輪 Event Loop 再執行,並返回 Promise.reject
。而後會進入業務端接口調用代碼的 catch 回調函數內,執行完業務異常處理後,若是沒有返回值,則表示無需再執行統一處理。相反,返回非 undefined
值,則表示還須要執行統一處理。
執行接口異常統一處理以前,先斷定 url 標識是否存在於 unhandleAPI
內,如存在,則執行統一處理。
以上是一個大體的設計思路,具體到實現還須要解決一些實際問題:
接口異常處理狀態存儲
使用一個數組對象 const unhandleAPI = []
保存全部已經調用但暫未響應的接口惟一標識,接口異常統一處理函以此斷定斷定是否須要執行。另外對外暴露一些操做 unhandleAPI
的接口。
const unhandleAPI = []; if (process.env.NODE_ENV !== 'production') { window.unhandleAPI = unhandleAPI; } export function matchUnhandleAPI(id) { return unhandleAPI.find(apiUid => apiUid === id); } export function addUnhandleAPI(id) { unhandleAPI.push(id); } export function removeUnhandleAPI(id) { const index = unhandleAPI.findIndex(apiUid => apiUid === id); if (process.env.NODE_ENV === 'production') { unhandleAPI.splice(index, 1); } else { // 方便非 production 環境查看接口處理狀況 unhandleAPI[index] += '#removed'; } }
發送接口請求
經過查看 axios 源碼,知道 axios 真正調用接口的方法是 axios.Axios.prototype.request
,因此須要對其進行覆寫。將當前調用接口的惟一標識添加到 unhandleAPI
數組對象內,同時也要添加到 axios.Axios.prototype.request
方法所返回的 Promise 實例對象當中(接口響應後的處理會使用到)。
let uid = 0; const axiosRequest = axios.Axios.prototype.request; axios.Axios.prototype.request = function(config) { uid += 1; const apiUid = `${config.url}?uid=${uid}`; // 接口調用的惟一標識 config.apiUid = apiUid; // 響應攔截器內須要使用到 apiUid,因此添加爲 config 屬性 addUnhandleAPI(apiUid); // 添加到接口異常處理狀態存儲的數組對象 const p = axiosRequest.call(this, config); // 觸發 axios 接口調用 p.apiUid = apiUid; // 在當前接口調用所返回的 Promise 實例中添加惟一標識屬性 return p; };
接口響應進入響應攔截器
在響應攔截器內斷定接口狀態,若是正常,則從接口狀態存儲的數組對象中移除當前響應接口的惟一標識。若是異常,則 setTimeout
延遲執行接口狀態異常統一處理函數,並返回 Promise.reject()
給到業務端。
service.interceptors.response.use( ({ data, config }) => { const { status, msg, data: result } = data; // 判斷接口狀態是否異常 if (status !== 0) { const pr = Promise.reject(data); pr.apiUid = config.apiUid; // Promise 實例中添加當前接口的惟一標識屬性 setTimeout(handleAPIStatusError, 0, pr, msg); // 異常先交由業務端處理,延遲執行統一處理函數 return pr; } // 接口狀態正常 removeUnhandleAPI(config.apiUid); // 從接口異常處理狀態存儲的數組對象中移除當前響應接口的惟一標識 return result; }, error => { Message.error(error.message); return Promise.reject(error); } );
業務端處理
如今假設接口狀態屬於異常狀況,通過響應攔截器以後,代碼執行到業務端,先看看業務端接口調用代碼:
callAPIMethod().catch(error => { // 業務端處理異常 });
以上是 Promise catch 的常規語法,此時若是 callAPIMethod 返回的 Promise 狀態爲 rejected,則會執行 catch 函數的回調函數。
還記得上文提到的流程上的難點嗎?
業務端決定是否執行接口異常統一處理函數,所以須要在此進行設計,catch 函數的回調函數如何進行聲明?其實上文已經提到 聲明 的設計方案,利用 catch 函數的回調函數的返回值。
設計方案 OK,落實到具體實現該如何進行代碼編寫?無疑,須要針對 catch 函數進行覆寫:
Promise.prototype.catch = function(onRejected) { function $onRejected(...args) { const catchResult = onRejected(...args); if (catchResult === undefined && this.apiUid) { removeUnhandleAPI(this.apiUid); } } return this.then(null, $onRejected.bind(this)); };
catch 方法自己其實只是語法糖,將 catch 函數的回調函數進行包裝,在包裝後的函數內,先執行業務端 catch 的回調函數,獲取到函數執行結果。接着,若是當前 promise 對象上有 apiUid 屬性,則表示當前 promise 是 API 層的 promise。若是 catch 的回調函數執行完畢以後的返回結果是 undefined
,則表示再也不須要執行接口異常狀態統一處理函數,相應的,須要從以前定義的 unhandleAPI
數組內移除當前接口的惟一標識。
then 方法返回新 promise
以上業務端處理看似正常,然而大多數狀況下,業務端代碼在接口調用以後不會直接鏈式調用 catch 方法,而是先調用 then 方法,再調用 catch 方法,以下:
callAPIMethod() .then(response => { // ... }) .catch(error => { // ... });
callAPIMethod()
的執行結果返回的是個 promise 對象,而且這個 promise 對象上會有 apiUid
屬性,表示當前 promise 是 API 層接口。而後鏈式調用 then 方法和 catch 方法,就由於中間插入了 then 方法的調用,致使 catch 的覆寫函數內 this
對象的屬性上沒有了 apiUid
屬性,也就沒法斷定當前 promise 是 API 層接口的返回對象。緣由是 then 方法執行完後返回了新的 Promise 實例,因此一樣須要對 then 方法進行覆寫。
const promiseThen = Promise.prototype.then; Promise.prototype.then = function(onFulfilled, onRejected) { // 獲取 then 方法返回的新 Promise 實例對象 const p = promiseThen.call(this, onFulfilled, onRejected); // 在 promise 對象上有 apiUid 的狀況下,表示是接口層的 Promise // 則給 then 方法返回的 Promise 實例對象也加上 apiUid if (this.apiUid) p.apiUid = this.apiUid; return p; };
then 方法的覆寫函數內,先執行原生的 then 方法,獲取返回結果,再判斷當前調用者 promise 對象是否有 apiUid
屬性。若是有,則表示是 API 層的 Promise,從而須要給當前 then 方法返回的 Promise 實例也添加上 apiUid
屬性。
執行接口異常狀態統一處理函數
接口異常狀態狀況下,若是業務端主動聲明須要執行接口異常狀態統一處理(業務端 catch 回調函數返回非 undefined
值),則在執行響應攔截器內 setTimeout
延遲執行的函數 handleAPIStatusError
時
只要接口響應狀態爲異常,都會執行接口異常狀態統一處理函數,內部會進行斷定
function handleAPIStatusError(pr, msg) { const index = unhandleAPI.findIndex(apiUid => apiUid === pr.apiUid); if (index >= 0) { pr.catch(() => { Message.error({ message: msg, duration: 5e3 }); }); } }
若是 unhandleAPI
數組對象內可以找到 pr.apiUid
,則表示須要執行接口異常狀態統一處理。
若是項目是由 vue-cli 搭建的 webpack 模板項目,在沒有修改 .babelrc 文件配置的狀況下,此方案在 Firefox 瀏覽器下是無效的。接口狀態異常的狀況下,老是會執行統一處理,不會先交由業務端處理異常,再斷定是否執行統一處理。
Firefox 下無效的緣由和解決方案我會在下一篇文章講解。
我的認爲這樣的設計仍是很優雅的,認知成本很是小,對小夥伴的常規開發沒有任何污染;對框架沒有任何依賴,可移植到任何框架項目下。
能力有限,哪位小夥伴有更加優雅合適的方案還望不吝賜教。