如何優雅的管理 HTTP 請求和響應攔截器?
最近重構一個老項目,發現其中處理請求的攔截器寫得相當亂,於是我將整個項目的請求處理層重構了,目前已經在項目中正常運行。
本文會和大家分享我的重構思路和後續優化的思考,爲方便與大家分享,我用 Vue3 實現一個簡單 demo,思路是一致的,有興趣的朋友可以在我 Github 查看,本文會以這個 Vue 實現的 demo 爲例介紹。
本文我會主要和大家分享以下幾點:
-
問題分析和方案設計;
-
重構後效果;
-
開發過程;
-
後期優化點;
如果你還不清楚什麼是 HTTP 請求和響應攔截器,那麼可以先看看《77.9K Star 的 Axios 項目有哪些值得借鑑的地方》 。
◆ 需求思考和方案設計
1. 問題分析
目前舊項目經過多位同事參與開發,攔截器存在以下問題:
-
代碼比較混亂,可讀性差;
-
每個攔截器職責混亂,存在相互依賴;
-
邏輯上存在問題;
-
團隊內部不同項目無法複用;
2. 方案設計
分析上面問題後,我初步的方案如下:
參考插件化架構設計,獨立每個攔截器,將每個攔截器抽離成單獨文件維護,做到職責單一,然後通過攔截器調度器進行調度和註冊。
其攔截器調度過程如下圖:
◆ 重構後效果
代碼其實比較簡單,這裏先看下最後實現效果:
1. 目錄分層更加清晰
重構後請求處理層的目錄分層更加清晰,大致如下:
2. 攔截器開發更加方便
在後續業務拓展新的攔截器,僅需 3 個步驟既可以完成攔截器的開發和使用,攔截器調度器會自動調用所有攔截器:
3. 每個攔截器職責更加單一,可插拔
將每個攔截器抽成一個文件去實現,讓每個攔截器職責分離且單一,當不需要使用某個攔截器時,隨時可以替換,靈活插拔。
◆ 開發過程
這裏以我單獨抽出來的這個 demo 項目爲例來介紹。
1. 初始化目錄結構
按照前面設計的方案,首先需要在項目中創建一下目錄結構:
- request
- index.js // 攔截器調度器
- interceptors
- request // 用來存放每個請求攔截器
- index.js // 管理所有請求攔截器,並做排序
- response // 用來存放每個響應攔截器
- index.js // 管理所有響應攔截器,並做排序
2. 定義攔截器調度器
因爲項目採用 axios 請求庫,所以我們需要先知道 axios 攔截器的使用方法,這裏簡單看下 axios 文檔上如何使用攔截器的:
// 添加請求攔截器
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);
});
從上面代碼,我們可以知道,使用攔截器的時候,只需調用 axios.interceptors
對象上對應方法即可,因此我們可以將這塊邏輯抽取出來:
// src/request/interceptors/index.js
import { log } from '../log';
import request from './request/index';
import response from './response/index';
export const runInterceptors = instance => {
log('[runInterceptors]', instance);
if(!instance) return;
// 設置請求攔截器
for (const key in request) {
instance.interceptors.request
.use(config => request[key](config));
}
// 設置響應攔截器
for (const key in response) {
instance.interceptors.response
.use(result => response[key](result));
}
return instance;
}
這就是我們的核心攔截器調度器,目前實現導入所有請求攔截器和響應攔截器後,通過 for
循環,註冊所有攔截器,最後將整個 axios 實例返回出去。
3. 定義簡單的請求攔截器和響應攔截器
這裏我們做簡單演示,創建以下兩個攔截器:
-
請求攔截器:setLoading,作用是在發起請求前,顯示一個全局 Toast 框,提示 “加載中...” 文案。
-
響應攔截器:setLoading,作用是在請求響應後,關閉頁面中的 Toast 框。
爲了統一開發規範,我們約定插件開發規範如下:
/*
攔截器名稱:xxx
*/
const interceptorName = options => {
log("[interceptor.request]interceptorName:", options);
// 攔截器業務
return options;
};
export default interceptorName;
首先創建文件 src/request/interceptors/request/
目錄下創建 setLoading.js
文件,按照上面約定的插件開發規範,我們完成下面插件開發:
// src/request/interceptors/request/setLoading.js
import { Toast } from 'vant';
import { log } from "../../log";
/*
攔截器名稱:全局設置請求的 loading 動畫
*/
const setLoading = options => {
log("[interceptor.request]setLoading:", options);
Toast.loading({
duration: 0,
message: '加載中...',
forbidClick: true,
});
return options;
};
export default setLoading;
然後在導出該請求攔截器,並且導出的是個數組,方便攔截器調度器進行統一註冊:
// src/request/interceptors/request/index.js
import setLoading from './setLoading';
export default [
setLoading
];
按照相同方式,我們開發響應攔截器:
// src/request/interceptors/response/setLoading.js
import { Toast } from 'vant';
import { log } from "../../log";
/*
攔截器名稱:關閉全局請求的 loading 動畫
*/
const setLoading = result => {
log("[interceptor.response]setLoading:", result);
// example: 請求返回成功時,關閉所有 toast 框
if(result && result.success){
Toast.clear();
}
return result;
};
export default setLoading;
導出響應攔截器:
// src/request/interceptors/response/index.js
import setLoading from './setLoading';
export default [
setLoading
];
4. 全局設置 axios 攔截器
按照前面相同步驟,我又多寫了幾個攔截器:
請求攔截器:
-
setSecurityInformation.js:爲請求的 url 添加安全參數;
-
setSignature.js:爲請求的請求頭添加加簽信息;
-
setToken.js:爲請求的請求頭添加 token 信息;
響應攔截器:
-
setError.js:處理響應結果的出錯情況,如關閉所有 toast 框;
-
setInvalid.js:處理響應結果的登錄失效情況,如跳轉到登錄頁;
-
setResult.js:處理響應結果的數據嵌套太深的問題,將
result.data.data.data
這類返回結果處理成result.data
格式;
至於是如何實現的,大家有興趣可以在我 Github 查看。
然後我們可以將 axios 進行二次封裝,導出 request
對象供業務使用:
// src/request/index.js
import axios from 'axios';
import { runInterceptors } from './interceptors/index';
export const requestConfig = { timeout: 10000 };
let request = axios.create(requestConfig);
request = runInterceptors(request);
export default request;
到這邊就完成。
在業務中需要發起請求,可以這麼使用:
<template>
<div><button @click="send">發起請求</button></div>
</template>
<script setup>
import request from './../request/index.js';
const send = async () => {
const result = await request({
url: 'https://httpbin.org/headers',
method: 'get'
})
}
</script>
5. 測試一下
開發到這邊就差不多,我們發送個請求,可以看到所有攔截器執行過程如下:
看看請求頭信息:
可以看到我們開發的請求攔截器已經生效。
◆ Taro 中使用
由於 Taro 中已經提供了 Taro.request 方法作爲請求方法,我們可以不需要使用 axios 發請求。
基於上面代碼進行改造,也很簡單,只需要更改 2 個地方:
1. 修改封裝請求的方法
主要是更換 axios 爲 Taro.request 方法,並使用 addInterceptor 方法導入攔截器:
// src/request/index.js
import Taro from "@tarojs/taro";
import { runInterceptors } from './interceptors/index';
Taro.addInterceptor(runInterceptors);
export const request = Taro.request;
export const requestTask = Taro.RequestTask; // 看需求,是否需要
export const addInterceptor = Taro.addInterceptor; // 看需求,是否需要
2. 修改攔截器調度器
由於 axios 和 Taro.request
添加攔截器的方法不同,所以也需要進行更換:
import request from './interceptors/request';
import response from './interceptors/response';
export const interceptor = {
request,
response
};
export const getInterceptor = (chain = {}) => {
// 設置請求攔截器
let requestParams = chain.requestParams;
for (const key in request) {
requestParams = request[key](requestParams);
}
// 設置響應攔截器
let responseObject = chain.proceed(requestParams);
for (const key in response) {
responseObject = responseObject.then(res => response[key](res));
}
return responseObject;
};
具體 API 可以看 Taro.request 文檔,這裏不過多介紹。
◆ 項目總結和思考
這次重構主要是按照已有業務進行重構,因此即使是重構後的請求層,仍然還有很多可以優化的點,目前我想到有這些,也算是我的一個 TODO LIST 了:
1. 將請求層獨立成庫
由於公司現在獨立站點的項目較多,考慮到項目的統一開發規範,可以考慮將該請求層獨立爲私有庫進行維護。
目前思路:
-
參考插件化架構設計,通過 lerna 做管理所有攔截器;
-
升級 TypeScript,方便管理和開發;
-
進行工程化改造,加入構建工具、單元測試、UMD 等等;
-
使用文檔和開發文檔完善。
2. 支持可更換請求庫
單獨抽這一點來講,是因爲目前我們前端團隊使用的請求庫較多,比較分散,所以考慮到通用性,需要增加支持可更換請求庫方法。
目前思路:
-
在已有請求層再抽象一層請求庫適配層,定義統一接口;
-
內置幾種常見請求庫的適配。
3. 開發攔截器腳手架
這個的目的其實很簡單,讓團隊內其他人直接使用腳手架工具,按照內置腳手架模版,快速創建一個攔截器,進行後續開發,很大程度統一攔截器的開發規範。
目前思路:
-
內置兩套攔截器模版:請求攔截器和響應攔截器;
-
腳手架開發比較簡單,參數(如語言)根據業務需要再確定。
4. 增強攔截器調度
目前實現的這個功能還比較簡單,還是得考慮增強攔截器調度。
目前思路:
-
處理攔截器失敗的情況;
-
處理攔截器調度順序的問題;
-
攔截器同步執行、異步執行、併發執行、循環執行等等情況;
-
可插拔的攔截器調度;
-
考慮參考 Tapable 插件機制;
◆ 本文總結
本文通過一次簡單的項目重構總結出一個請求層攔截器調度方案,目的是爲了實現所有攔截器職責單一、方便維護,並統一維護和自動調度,大大降低實際業務的攔截器開發上手難度。
後續我仍有很多需要優化的地方,作爲自己的一個 TODO LIST,如果是做成完全通用,則定位可能更偏向於攔截器調度容器,只提供一些通用攔截器,其餘還是由開發者定義,庫負責調度,但常用的請求庫一般都已經做好,所以這樣做的價值有待權衡。
當然,目前還是優先作爲團隊內部私有庫進行開發和使用,因爲基本上團隊內部使用的業務都差不多,只是項目不同。
來源:
https://segmentfault.com/a/1190000040366591
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1umgMhg9yXwZ88eKhhWRkw