如何優雅的管理 HTTP 請求和響應攔截器?

最近重構一個老項目,發現其中處理請求的攔截器寫得相當亂,於是我將整個項目的請求處理層重構了,目前已經在項目中正常運行。

本文會和大家分享我的重構思路和後續優化的思考,爲方便與大家分享,我用 Vue3 實現一個簡單 demo,思路是一致的,有興趣的朋友可以在我 Github 查看,本文會以這個 Vue 實現的 demo 爲例介紹。

本文我會主要和大家分享以下幾點:

  1. 問題分析和方案設計;

  2. 重構後效果;

  3. 開發過程;

  4. 後期優化點;

如果你還不清楚什麼是 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. 定義簡單的請求攔截器和響應攔截器

這裏我們做簡單演示,創建以下兩個攔截器:

  1. 請求攔截器:setLoading,作用是在發起請求前,顯示一個全局 Toast 框,提示 “加載中...” 文案。

  2. 響應攔截器: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 攔截器

按照前面相同步驟,我又多寫了幾個攔截器:
請求攔截器:

響應攔截器:

至於是如何實現的,大家有興趣可以在我 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. 將請求層獨立成庫

由於公司現在獨立站點的項目較多,考慮到項目的統一開發規範,可以考慮將該請求層獨立爲私有庫進行維護。
目前思路:

2. 支持可更換請求庫

單獨抽這一點來講,是因爲目前我們前端團隊使用的請求庫較多,比較分散,所以考慮到通用性,需要增加支持可更換請求庫方法。
目前思路:

3. 開發攔截器腳手架

這個的目的其實很簡單,讓團隊內其他人直接使用腳手架工具,按照內置腳手架模版,快速創建一個攔截器,進行後續開發,很大程度統一攔截器的開發規範。
目前思路:

4. 增強攔截器調度

目前實現的這個功能還比較簡單,還是得考慮增強攔截器調度。
目前思路:

◆ 本文總結

本文通過一次簡單的項目重構總結出一個請求層攔截器調度方案,目的是爲了實現所有攔截器職責單一、方便維護,並統一維護和自動調度,大大降低實際業務的攔截器開發上手難度。

後續我仍有很多需要優化的地方,作爲自己的一個 TODO LIST,如果是做成完全通用,則定位可能更偏向於攔截器調度容器,只提供一些通用攔截器,其餘還是由開發者定義,庫負責調度,但常用的請求庫一般都已經做好,所以這樣做的價值有待權衡。

當然,目前還是優先作爲團隊內部私有庫進行開發和使用,因爲基本上團隊內部使用的業務都差不多,只是項目不同。

來源:

https://segmentfault.com/a/1190000040366591

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1umgMhg9yXwZ88eKhhWRkw