ahooks 是怎麼解決用戶多次提交問題?

本文是深入淺出 ahooks 源碼系列文章的第四篇,這個系列的目標主要有以下幾點:

注:本系列對 ahooks 的源碼解析是基於 v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見 詳情 [1]。

系列文章:

本文來探索一下 ahooks 的 useLockFn。

場景

試想一下,有這麼一個場景,有一個表單,你可能多次提交,就很可能導致結果不正確。

解決這類問題的方法有很多,比如添加 loading,在第一次點擊之後就無法再次點擊。另外一種方法就是給請求異步函數添加上一個靜態鎖,防止併發產生。這就是 ahooks 的 useLockFn 做的事情。

useLockFn

useLockFn 用於給一個異步函數增加競態鎖,防止併發執行。

它的源碼比較簡單,如下所示:

import { useRef, useCallback } from 'react';

// 用於給一個異步函數增加競態鎖,防止併發執行。
function useLockFn<P extends any[] = any[], V extends any = any>(fn: (...args: P) => Promise<V>) {
  // 是否現在處於一個鎖中
  const lockRef = useRef(false);
  // 返回的是增加了競態鎖的函數
  return useCallback(
    async (...args: P) ={
      // 判斷請求是否正在進行
      if (lockRef.current) return;
      // 請求中
      lockRef.current = true;
      try {
        // 執行原有請求
        const ret = await fn(...args);
        // 請求完成,狀態鎖設置爲 false
        lockRef.current = false;
        return ret;
      } catch (e) {
        // 請求失敗,狀態鎖設置爲 false
        lockRef.current = false;
        throw e;
      }
    },
    [fn],
  );
}

export default useLockFn;

可以看到,它的入參是異步函數,返回的是一個增加了競態鎖的函數。通過 lockRef 做一個標識位,初始化的時候它的值爲 false。當正在請求,則設置爲 true,從而下次再調用這個函數的時候,就直接 return,不執行原函數,從而達到加鎖的目的。

缺點

雖然實用,但缺點很明顯,我需要給每一個需要添加競態鎖的請求異步函數都手動加一遍。那有沒有比較通用和方便的方法呢?

答案是可以通過 axios 自動取消重複請求。

axios 自動取消重複請求

axios 取消請求

對於原生的 XMLHttpRequest 對象發起的 HTTP 請求,可以調用 XMLHttpRequest 對象的 abort 方法。

那麼我們項目中常用的 axios 呢?它其實底層也是用的 XMLHttpRequest 對象,它對外暴露取消請求的 API 是 CancelToken。可以使用如下:

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

axios.post('/user/12345'{
  name: 'gopal'
}{
  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 的攔截器。

具體的做法如下:

第一步,定義幾個重要的輔助函數。

這裏我認爲,如果有需要的話,可以暴露一個 API 給開發者進行自定義重複的規則。這裏我們先根據請求方法、url、以及參數生成唯一的 key 去做。

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);
  }
}

第二步,添加請求攔截器。

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

第二步,添加響應攔截器。

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);
   }
);

到這一步,我們就通過 axios 完成了自動取消重複請求的功能。

思考與總結

雖然可以通過類似 useLockFn 這樣的 hook 或方法給請求函數添加競態鎖的方式解決重複請求的問題。但這種還是需要依賴於開發者的習慣,如果沒有一些規則的約束,很難避免問題。

通過 axios 攔截器以及其 CancelToken 功能,我們能夠在攔截器中自動將已發的請求取消,當然假如有一些接口就是需要重複發送請求,可以考慮加一下白名單功能,讓請求不進行取消。

參考

參考資料

[1]

詳情: https://github.com/GpingFeng/hooks

[2]

大家都能看得懂的源碼(一)ahooks 整體架構篇: https://juejin.cn/post/7105396478268407815

[3]

如何使用插件化機制優雅的封裝你的請求 hook : https://juejin.cn/post/7105733829972721677

[4]

ahooks 是怎麼解決 React 的閉包問題的?: https://juejin.cn/post/7106061970184339464

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