Axios 如何取消重複請求?

在 Web 項目開發過程中,我們經常會遇到重複請求的場景,如果系統不對重複的請求進行處理,則可能會導致系統出現各種問題。比如重複的 post 請求可能會導致服務端產生兩筆記錄。那麼重複請求是如何產生的呢?這裏我們舉 2 個常見的場景:

既然已經知道重複請求是如何產生的,也知道了它會帶來一些問題。接下來,阿寶哥將以 Axios 爲例,帶大家來一起解決重複請求的問題。

一、如何取消請求

Axios 是一個基於 Promise 的 HTTP 客戶端,同時支持瀏覽器和 Node.js 環境。它是一個優秀的 HTTP 客戶端,被廣泛地應用在大量的 Web 項目中。對於瀏覽器環境來說,Axios 底層是利用 XMLHttpRequest 對象來發起 HTTP 請求。如果要取消請求的話,我們可以通過調用 XMLHttpRequest 對象上的 abort 方法來取消請求:

let xhr = new XMLHttpRequest();
xhr.open("GET", "https://developer.mozilla.org/", true);
xhr.send();
setTimeout(() => xhr.abort(), 300);

而對於 Axios 來說,我們可以通過 Axios 內部提供的 CancelToken 來取消請求:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.post('/user/12345', {
  name: 'semlinker'
}, {
  cancelToken: source.token
})

source.cancel('Operation canceled by the user.'); // 取消請求,參數是可選的

此外,你也可以通過調用 CancelToken 的構造函數來創建 CancelToken,具體如下所示:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

cancel(); // 取消請求

現在我們已經知道在 Axios 中如何使用 CancelToken 來取消請求了,那麼 CancelToken 內部是如何工作的呢?這裏我們先記住這個問題,後面阿寶哥將爲你們揭開 CancelToken 背後的祕密。接下來,我們來分析一下如何判斷重複請求。

二、如何判斷重複請求

當請求方式、請求 URL 地址和請求參數都一樣時,我們就可以認爲請求是一樣的。因此在每次發起請求時,我們就可以根據當前請求的請求方式、請求 URL 地址和請求參數來生成一個唯一的 key,同時爲每個請求創建一個專屬的 CancelToken,然後把 key 和 cancel 函數以鍵值對的形式保存到 Map 對象中,使用 Map 的好處是可以快速的判斷是否有重複的請求:

import qs from 'qs'

const pendingRequest = new Map();
// GET -> params;POST -> data
const requestKey = [method, url, qs.stringify(params), qs.stringify(data)].join('&'); 
const cancelToken = new CancelToken(function executor(cancel) {
  if(!pendingRequest.has(requestKey)){
    pendingRequest.set(requestKey, cancel);
  }
})

當出現重複請求的時候,我們就可以使用 cancel 函數來取消前面已經發出的請求,在取消請求之後,我們還需要把取消的請求從 pendingRequest 中移除。現在我們已經知道如何取消請求和如何判斷重複請求,下面我們來介紹如何取消重複請求。

三、如何取消重複請求

因爲我們需要對所有的請求都進行處理,所以我們可以考慮使用 Axios 的攔截器機制來實現取消重複請求的功能。Axios 爲開發者提供了請求攔截器和響應攔截器,它們的作用如下:

3.1 定義輔助函數

在配置請求攔截器和響應攔截器前,阿寶哥先來定義 3 個輔助函數:

function generateReqKey(config) {
  const { method, url, params, data } = config;
  return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}
const pendingRequest = new Map();
function addPendingRequest(config) {
  const requestKey = generateReqKey(config);
  config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
    if (!pendingRequest.has(requestKey)) {
       pendingRequest.set(requestKey, cancel);
    }
  });
}
function removePendingRequest(config) {
  const requestKey = generateReqKey(config);
  if (pendingRequest.has(requestKey)) {
     const cancelToken = pendingRequest.get(requestKey);
     cancelToken(requestKey);
     pendingRequest.delete(requestKey);
  }
}

創建好 generateReqKeyaddPendingRequestremovePendingRequest 函數之後,我們就可以設置請求攔截器和響應攔截器了。

3.2 設置請求攔截器

axios.interceptors.request.use(
  function (config) {
    removePendingRequest(config); // 檢查是否存在重複請求,若存在則取消已發的請求
    addPendingRequest(config); // 把當前請求信息添加到pendingRequest對象中
    return config;
  },
  (error) => {
     return Promise.reject(error);
  }
);

3.3 設置響應攔截器

axios.interceptors.response.use(
  (response) => {
     removePendingRequest(response.config); // 從pendingRequest對象中移除請求
     return response;
   },
   (error) => {
      removePendingRequest(error.config || {}); // 從pendingRequest對象中移除請求
      if (axios.isCancel(error)) {
        console.log("已取消的重複請求:" + error.message);
      } else {
        // 添加異常處理
      }
      return Promise.reject(error);
   }
);

由於完整的示例代碼內容比較多,阿寶哥就不放具體的代碼了。感興趣的小夥伴,可以訪問以下地址瀏覽示例代碼。

完整的示例代碼:https://gist.github.com/semlinker/e426780664f0186db434882f1e27ac3a

這裏我們來看一下 Axios 取消重複請求示例的運行結果:

從上圖可知,當出現重複請求時,之前已發送且未完成的請求會被取消掉。下面我們用一張流程圖來總結一下取消重複請求的處理流程:

最後,我們來回答前面留下的問題,即 CancelToken 內部是如何工作的?

四、CancelToken 的工作原理

在前面的示例中,我們是通過調用 CancelToken 構造函數來創建 CancelToken 對象:

new axios.CancelToken((cancel) => {
  if (!pendingRequest.has(requestKey)) {
    pendingRequest.set(requestKey, cancel);
  }
})

所以接下來,我們來分析 CancelToken 構造函數,該函數被定義在 lib/cancel/CancelToken.js 文件中:

// lib/cancel/CancelToken.js
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) { // 設置cancel對象
    if (token.reason) {
      return; // Cancellation has already been requested
    }
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

由以上代碼可知,cancel 對象是一個函數,當我們調用該函數後,會創建 Cancel 對象並調用 resolvePromise 方法。該方法執行後,CancelToken 對象上 promise 屬性所指向的 promise 對象的狀態將變爲 resolved。那麼這樣做的目的是什麼呢?這裏我們從 lib/adapters/xhr.js 文件中找到了答案:

// lib/adapters/xhr.js 
if (config.cancelToken) {
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) { return; }
    request.abort(); // 取消請求
    reject(cancel);
    request = null;
  });
}

看完上述的內容,可能有的小夥伴還不是很能理解 CancelToken 的工作原理,所以阿寶哥又畫了一張圖來幫助大家理解 CancelToken 的工作原理:

五、總結

本文介紹了在 Axios 中如何取消重複請求及 CancelToken 的工作原理,在後續的文章中,阿寶哥將會介紹在 Axios 中如何設置數據緩存,感興趣的小夥伴不要錯過喲。如果你想了解 Axios 中 HTTP 攔截器及 HTTP 適配器的設計與實現,可以閱讀 77.9K 的 Axios 項目有哪些值得借鑑的地方 這篇文章。

六、參考資源

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