如何優(yōu)雅的管理 HTTP 請(qǐng)求和響應(yīng)攔截器?【經(jīng)驗(yàn)總結(jié)篇】

本文思路來自實(shí)際項(xiàng)目的重構(gòu)總結(jié),歡迎糾正和交流。
最近重構(gòu)一個(gè)老項(xiàng)目,發(fā)現(xiàn)其中處理請(qǐng)求的攔截器寫得相當(dāng)亂,于是我將整個(gè)項(xiàng)目的請(qǐng)求處理層重構(gòu)了,目前已經(jīng)在項(xiàng)目中正常運(yùn)行。
本文會(huì)和大家分享我的重構(gòu)思路和后續(xù)優(yōu)化的思考,為方便與大家分享,我用 Vue3 實(shí)現(xiàn)一個(gè)簡單 demo,思路是一致的,有興趣的朋友可以在我 Github 查看[1],本文會(huì)以這個(gè) Vue 實(shí)現(xiàn)的 demo 為例介紹。
本文我會(huì)主要和大家分享以下幾點(diǎn):
問題分析和方案設(shè)計(jì); 重構(gòu)后效果; 開發(fā)過程; 后期優(yōu)化點(diǎn);
如果你還不清楚什么是 HTTP 請(qǐng)求和響應(yīng)攔截器,那么可以先看看《77.9K Star 的 Axios 項(xiàng)目有哪些值得借鑒的地方》[2] 。
一、需求思考和方案設(shè)計(jì)
1. 問題分析
目前舊項(xiàng)目經(jīng)過多位同事參與開發(fā),攔截器存在以下問題:
代碼比較混亂,可讀性差; 每個(gè)攔截器職責(zé)混亂,存在相互依賴; 邏輯上存在問題; 團(tuán)隊(duì)內(nèi)部不同項(xiàng)目無法復(fù)用;
2. 方案設(shè)計(jì)
分析上面問題后,我初步的方案如下:參考插件化架構(gòu)設(shè)計(jì),獨(dú)立每個(gè)攔截器,將每個(gè)攔截器抽離成單獨(dú)文件維護(hù),做到職責(zé)單一,然后通過攔截器調(diào)度器進(jìn)行調(diào)度和注冊(cè)。
其攔截器調(diào)度過程如下圖:
二、重構(gòu)后效果
代碼其實(shí)比較簡單,這里先看下最后實(shí)現(xiàn)效果:
1. 目錄分層更加清晰
重構(gòu)后請(qǐng)求處理層的目錄分層更加清晰,大致如下:

2. 攔截器開發(fā)更加方便
在后續(xù)業(yè)務(wù)拓展新的攔截器,僅需 3 個(gè)步驟既可以完成攔截器的開發(fā)和使用,攔截器調(diào)度器會(huì)自動(dòng)調(diào)用所有攔截器:

3. 每個(gè)攔截器職責(zé)更加單一,可插拔
將每個(gè)攔截器抽成一個(gè)文件去實(shí)現(xiàn),讓每個(gè)攔截器職責(zé)分離且單一,當(dāng)不需要使用某個(gè)攔截器時(shí),隨時(shí)可以替換,靈活插拔。
三、開發(fā)過程
這里以我單獨(dú)抽出來的這個(gè) demo 項(xiàng)目[3]為例來介紹。
1. 初始化目錄結(jié)構(gòu)
按照前面設(shè)計(jì)的方案,首先需要在項(xiàng)目中創(chuàng)建一下目錄結(jié)構(gòu):
- request
- index.js // 攔截器調(diào)度器
- interceptors
- request // 用來存放每個(gè)請(qǐng)求攔截器
- index.js // 管理所有請(qǐng)求攔截器,并做排序
- response // 用來存放每個(gè)響應(yīng)攔截器
- index.js // 管理所有響應(yīng)攔截器,并做排序
2. 定義攔截器調(diào)度器
因?yàn)轫?xiàng)目采用 axios 請(qǐng)求庫[4],所以我們需要先知道 axios 攔截器的使用方法,這里簡單看下 axios 文檔上如何使用攔截器[5]的:
// 添加請(qǐng)求攔截器
axios.interceptors.request.use(function (config) {
// 業(yè)務(wù) 邏輯
return config;
}, function (error) {
// 業(yè)務(wù) 邏輯
return Promise.reject(error);
});
// 添加響應(yīng)攔截器
axios.interceptors.response.use(function (response) {
// 業(yè)務(wù) 邏輯
return response;
}, function (error) {
// 業(yè)務(wù)邏輯
return Promise.reject(error);
});
從上面代碼,我們可以知道,使用攔截器的時(shí)候,只需調(diào)用 axios.interceptors 對(duì)象上對(duì)應(yīng)方法即可,因此我們可以將這塊邏輯抽取出來:
// 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;
// 設(shè)置請(qǐng)求攔截器
for (const key in request) {
instance.interceptors.request
.use(config => request[key](config "key"));
}
// 設(shè)置響應(yīng)攔截器
for (const key in response) {
instance.interceptors.response
.use(result => response[key](result "key"));
}
return instance;
}
這就是我們的核心攔截器調(diào)度器,目前實(shí)現(xiàn)導(dǎo)入所有請(qǐng)求攔截器和響應(yīng)攔截器后,通過 for 循環(huán),注冊(cè)所有攔截器,最后將整個(gè) axios 實(shí)例返回出去。
3. 定義簡單的請(qǐng)求攔截器和響應(yīng)攔截器
這里我們做簡單演示,創(chuàng)建以下兩個(gè)攔截器:
請(qǐng)求攔截器:setLoading,作用是在發(fā)起請(qǐng)求前,顯示一個(gè)全局 Toast 框,提示“加載中...”文案。 響應(yīng)攔截器:setLoading,作用是在請(qǐng)求響應(yīng)后,關(guān)閉頁面中的 Toast 框。
為了統(tǒng)一開發(fā)規(guī)范,我們約定插件開發(fā)規(guī)范如下:
/*
攔截器名稱:xxx
*/
const interceptorName = options => {
log("[interceptor.request]interceptorName:", options);
// 攔截器業(yè)務(wù)
return options;
};
export default interceptorName;
首先創(chuàng)建文件 src/request/interceptors/request/ 目錄下創(chuàng)建 setLoading.js 文件,按照上面約定的插件開發(fā)規(guī)范,我們完成下面插件開發(fā):
// src/request/interceptors/request/setLoading.js
import { Toast } from 'vant';
import { log } from "../../log";
/*
攔截器名稱:全局設(shè)置請(qǐng)求的 loading 動(dòng)畫
*/
const setLoading = options => {
log("[interceptor.request]setLoading:", options);
Toast.loading({
duration: 0,
message: '加載中...',
forbidClick: true,
});
return options;
};
export default setLoading;
然后在導(dǎo)出該請(qǐng)求攔截器,并且導(dǎo)出的是個(gè)數(shù)組,方便攔截器調(diào)度器進(jìn)行統(tǒng)一注冊(cè):
// src/request/interceptors/request/index.js
import setLoading from './setLoading';
export default [
setLoading
];
按照相同方式,我們開發(fā)響應(yīng)攔截器:
// src/request/interceptors/response/setLoading.js
import { Toast } from 'vant';
import { log } from "../../log";
/*
攔截器名稱:關(guān)閉全局請(qǐng)求的 loading 動(dòng)畫
*/
const setLoading = result => {
log("[interceptor.response]setLoading:", result);
// example: 請(qǐng)求返回成功時(shí),關(guān)閉所有 toast 框
if(result && result.success){
Toast.clear();
}
return result;
};
export default setLoading;
導(dǎo)出響應(yīng)攔截器:
// src/request/interceptors/response/index.js
import setLoading from './setLoading';
export default [
setLoading
];
4. 全局設(shè)置 axios 攔截器
按照前面相同步驟,我又多寫了幾個(gè)攔截器:請(qǐng)求攔截器:
setSecurityInformation.js:為請(qǐng)求的 url 添加安全參數(shù); setSignature.js:為請(qǐng)求的請(qǐng)求頭添加加簽信息; setToken.js:為請(qǐng)求的請(qǐng)求頭添加 token 信息;
響應(yīng)攔截器:
setError.js:處理響應(yīng)結(jié)果的出錯(cuò)情況,如關(guān)閉所有 toast 框; setInvalid.js:處理響應(yīng)結(jié)果的登錄失效情況,如跳轉(zhuǎn)到登錄頁; setResult.js:處理響應(yīng)結(jié)果的數(shù)據(jù)嵌套太深的問題,將 result.data.data.data這類返回結(jié)果處理成result.data格式;
至于是如何實(shí)現(xiàn)的,大家有興趣可以在我 Github 查看[6]。
然后我們可以將 axios 進(jìn)行二次封裝,導(dǎo)出 request 對(duì)象供業(yè)務(wù)使用:
// 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;
到這邊就完成。
在業(yè)務(wù)中需要發(fā)起請(qǐng)求,可以這么使用:
<template>
<div><button @click="send">發(fā)起請(qǐng)求</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. 測試一下
開發(fā)到這邊就差不多,我們發(fā)送個(gè)請(qǐng)求,可以看到所有攔截器執(zhí)行過程如下:

看看請(qǐng)求頭信息:

可以看到我們開發(fā)的請(qǐng)求攔截器已經(jīng)生效。
四、Taro 中使用
由于 Taro[7] 中已經(jīng)提供了 Taro.request[8] 方法作為請(qǐng)求方法,我們可以不需要使用 axios 發(fā)請(qǐng)求。
基于上面代碼進(jìn)行改造,也很簡單,只需要更改 2 個(gè)地方:
1. 修改封裝請(qǐng)求的方法
主要是更換 axios 為 Taro.request 方法,并使用 addInterceptor 方法導(dǎo)入攔截器:
// 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. 修改攔截器調(diào)度器
由于 axios 和 Taro.request 添加攔截器的方法不同,所以也需要進(jìn)行更換:
import request from './interceptors/request';
import response from './interceptors/response';
export const interceptor = {
request,
response
};
export const getInterceptor = (chain = {}) => {
// 設(shè)置請(qǐng)求攔截器
let requestParams = chain.requestParams;
for (const key in request) {
requestParams = request[key](requestParams "key");
}
// 設(shè)置響應(yīng)攔截器
let responseObject = chain.proceed(requestParams);
for (const key in response) {
responseObject = responseObject.then(res => response[key](res "key"));
}
return responseObject;
};
具體 API 可以看 Taro.request[9] 文檔,這里不過多介紹。
五、項(xiàng)目總結(jié)和思考
這次重構(gòu)主要是按照已有業(yè)務(wù)進(jìn)行重構(gòu),因此即使是重構(gòu)后的請(qǐng)求層,仍然還有很多可以優(yōu)化的點(diǎn),目前我想到有這些,也算是我的一個(gè) TODO LIST 了:
1. 將請(qǐng)求層獨(dú)立成庫
由于公司現(xiàn)在獨(dú)立站點(diǎn)的項(xiàng)目較多,考慮到項(xiàng)目的統(tǒng)一開發(fā)規(guī)范,可以考慮將該請(qǐng)求層獨(dú)立為私有庫進(jìn)行維護(hù)。目前思路:
參考插件化架構(gòu)設(shè)計(jì),通過 lerna[10] 做管理所有攔截器; 升級(jí) TypeScript,方便管理和開發(fā); 進(jìn)行工程化改造,加入構(gòu)建工具、單元測試、UMD等等; 使用文檔和開發(fā)文檔完善。
2. 支持可更換請(qǐng)求庫
單獨(dú)抽這一點(diǎn)來講,是因?yàn)槟壳拔覀兦岸藞F(tuán)隊(duì)使用的請(qǐng)求庫較多,比較分散,所以考慮到通用性,需要增加支持可更換請(qǐng)求庫方法。目前思路:
在已有請(qǐng)求層再抽象一層請(qǐng)求庫適配層,定義統(tǒng)一接口; 內(nèi)置幾種常見請(qǐng)求庫的適配。
3. 開發(fā)攔截器腳手架
這個(gè)的目的其實(shí)很簡單,讓團(tuán)隊(duì)內(nèi)其他人直接使用腳手架工具,按照內(nèi)置腳手架模版,快速創(chuàng)建一個(gè)攔截器,進(jìn)行后續(xù)開發(fā),很大程度統(tǒng)一攔截器的開發(fā)規(guī)范。目前思路:
內(nèi)置兩套攔截器模版:請(qǐng)求攔截器和響應(yīng)攔截器; 腳手架開發(fā)比較簡單,參數(shù)(如語言)根據(jù)業(yè)務(wù)需要再確定。
4. 增強(qiáng)攔截器調(diào)度
目前實(shí)現(xiàn)的這個(gè)功能還比較簡單,還是得考慮增強(qiáng)攔截器調(diào)度。目前思路:
處理攔截器失敗的情況; 處理攔截器調(diào)度順序的問題; 攔截器同步執(zhí)行、異步執(zhí)行、并發(fā)執(zhí)行、循環(huán)執(zhí)行等等情況; 可插拔的攔截器調(diào)度; 考慮參考 Tapable 插件機(jī)制;
六、本文總結(jié)
本文通過一次簡單的項(xiàng)目重構(gòu)總結(jié)出一個(gè)請(qǐng)求層攔截器調(diào)度方案,目的是為了實(shí)現(xiàn)所有攔截器職責(zé)單一、方便維護(hù),并統(tǒng)一維護(hù)和自動(dòng)調(diào)度,大大降低實(shí)際業(yè)務(wù)的攔截器開發(fā)上手難度。
后續(xù)我仍有很多需要優(yōu)化的地方,作為自己的一個(gè) TODO LIST,如果是做成完全通用,則定位可能更偏向于攔截器調(diào)度容器,只提供一些通用攔截器,其余還是由開發(fā)者定義,庫負(fù)責(zé)調(diào)度,但常用的請(qǐng)求庫一般都已經(jīng)做好,所以這樣做的價(jià)值有待權(quán)衡。
當(dāng)然,目前還是優(yōu)先作為團(tuán)隊(duì)內(nèi)部私有庫進(jìn)行開發(fā)和使用,因?yàn)榛旧蠄F(tuán)隊(duì)內(nèi)容使用的業(yè)務(wù)都差不多,只是項(xiàng)目不同。
參考資料
在我 Github 查看: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Summary/useful-request-demo/index.html
[2]《77.9K Star 的 Axios 項(xiàng)目有哪些值得借鑒的地方》: https://juejin.cn/post/6885471967714115597
[3]這個(gè) demo 項(xiàng)目: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Summary/useful-request-demo/index.html
[4]axios 請(qǐng)求庫: https://github.com/axios/axios
[5]axios 文檔上如何使用攔截器: https://github.com/axios/axios#interceptors
[6]在我 Github 查看: https://github.com/pingan8787/Leo-JavaScript/blob/master/Cute-Summary/useful-request-demo/index.html
[7]Taro: https://taro-docs.jd.com/
[8]Taro.request: https://taro-docs.jd.com/taro/docs/2.x/apis/network/request/request
[9]Taro.request: https://taro-docs.jd.com/taro/docs/2.x/apis/network/request/request
[10]lerna: https://github.com/lerna/lerna/
往期精文
2K+ Star!超過 50 個(gè)專題、450 個(gè)好項(xiàng)目,推薦過的重磅項(xiàng)目合集
Vue3 的學(xué)習(xí)教程匯總、源碼解釋項(xiàng)目、支持的 UI 組件庫、優(yōu)質(zhì)實(shí)戰(zhàn)項(xiàng)目
微信搜索 全棧修煉,回復(fù) 電子書 就送你 1000+ 本精華編程電子書;回復(fù) 1024 送你一套完整的 前端 視頻教程,絕對(duì)免費(fèi),無套路獲取。。

