如何防止重複發送ajax請求

背景

先來講說重複發送ajax請求帶來的問題node

  • 場景一:用戶快速點擊按鈕,屢次相同的請求打到服務器,給服務器形成壓力。若是碰到提交表單操做,並且剛好後端沒有作兼容處理,那麼可能會形成數據庫中插入兩條及以上的相同數據
  • 場景二:用戶頻繁切換下拉篩選條件,第一次篩選數據量較多,花費的時間較長,第二次篩選數據量較少,請求後發先至,內容先顯示在界面上。可是等到第一次的數據回來以後,就會覆蓋掉第二次的顯示的數據。篩選結果和查詢條件不一致,用戶體驗很很差

經常使用解決方案

爲了解決上述問題,一般會採用如下幾種解決方案ios

  • 狀態變量git

    發送ajax請求前,btnDisable置爲true,禁止按鈕點擊,等到ajax請求結束解除限制,這是咱們最經常使用的一種方案 github

    但該方案也存在如下弊端:

    • 與業務代碼耦合度高
    • 沒法解決上述場景二存在的問題
  • 函數節流和函數防抖ajax

    固定的一段時間內,只容許執行一次函數,若是有重複的函數調用,能夠選擇使用函數節流忽略後面的函數調用,以此來解決場景一存在的問題 數據庫

    也能夠選擇使用函數防抖忽略前面的函數調用,以此來解決場景二存在的問題
    該方案能覆蓋場景一和場景二,不過也存在一個大問題:

    • wait time是一個固定時間,而ajax請求的響應時間不固定,wait time設置小於ajax響應時間,兩個ajax請求依舊會存在重疊部分,wait time設置大於ajax響應時間,影響用戶體驗。總之就是wait time的時間設定是個難題

請求攔截和請求取消

做爲一個成熟的ajax應用,它應該能本身在pending過程當中選擇請求攔截和請求取消編程

  • 請求攔截axios

    用一個數組存儲目前處於pending狀態的請求。發送請求前先判斷這個api請求以前是否已經有還在pending的同類,便是否存在上述數組中,若是存在,則不發送請求,不存在就正常發送而且將該api添加到數組中。等請求完結後刪除數組中的這個api。後端

  • 請求取消api

    用一個數組存儲目前處於pending狀態的請求。發送請求時判斷這個api請求以前是否已經有還在pending的同類,便是否存在上述數組中,若是存在,則找到數組中pending狀態的請求並取消,不存在就將該api添加到數組中。而後發送請求,等請求完結後刪除數組中的這個api

實現

接下來介紹一下本文的主角 axioscancel token(查看詳情)。經過axioscancel token,咱們能夠輕鬆作到請求攔截和請求取消

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

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

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

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
複製代碼

官網示例中,先定義了一個 const CancelToken = axios.CancelToken,定義能夠在axios源碼axios/lib/axios.js目錄下找到

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
複製代碼

示例中調用了axios.CancelToken的source方法,因此接下來咱們再去axios/lib/cancel/CancelToken.js目錄下看看source方法

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};
複製代碼

source方法返回一個具備tokencancel屬性的對象,這兩個屬性都和CancelToken構造函數有關聯,因此接下來咱們再看看CancelToken構造函數

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @class
 * @param {Function} executor The executor function.
 */
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) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}
複製代碼

因此souce.token是一個CancelToken的實例,而source.cancel是一個函數,調用它會在CancelToken的實例上添加一個reason屬性,而且將實例上的promise狀態resolve掉

官網另外一個示例

const CancelToken = axios.CancelToken;
let cancel;

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

// cancel the request
cancel();
複製代碼

它與第一個示例的區別就在於每一個請求都會建立一個CancelToken實例,從而它擁有多個cancel函數來執行取消操做

咱們執行axios.get,最後實際上是執行axios實例上的request方法,方法定義在axios\lib\core\Axios.js

Axios.prototype.request = function request(config) {
  ...
  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
複製代碼

request方法返回一個鏈式調用的promise,等同於

Promise.resolve(config).then('request攔截器中的resolve方法', 'request攔截器中的rejected方法').then(dispatchRequest, undefined).then('response攔截器中的resolve方法', 'response攔截器中的rejected方法')
複製代碼

在閱讀源碼的過程當中,這些編程小技巧都是很是值得學些的

接下來看看axios\lib\core\dispatchRequest.js中的dispatchRequest方法

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  ...
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then()
};
複製代碼

若是是cancel方法當即執行,建立了CancelToken實例上的reason屬性,那麼就會拋出異常,從而被response攔截器中的rejected方法捕獲,並不會發送請求,這個能夠用來作請求攔截

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};
複製代碼

若是cancel方法延遲執行,那麼咱們接着去找axios\lib\defaults.js中的defaults.adapter

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter()
}
複製代碼

終於找到axios\lib\adapters\xhr.js中的xhrAdapter

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    ...
    var request = new XMLHttpRequest();
    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    // Send the request
    request.send(requestData);
  })
}
複製代碼

能夠看到xhrAdapter建立了XMLHttpRequest對象,發送ajax請求,在這以後若是執行cancel函數將cancelToken.promise狀態resolve掉,就會調用request.abort(),能夠用來請求取消

解耦

剩下要作的就是將cancelToken從業務代碼中剝離出來。咱們在項目中,大多都會對axios庫再作一層封裝來處理一些公共邏輯,最多見的就是在response攔截器裏統一處理返回code。那麼咱們固然也能夠將cancelToken的配置放在request攔截器。可參考demo

let pendingAjax = []
const FAST_CLICK_MSG = '數據請求中,請稍後'
const CancelToken = axios.CancelToken
const removePendingAjax = (url, type) => {
  const index = pendingAjax.findIndex(i => i.url === url)
  if (index > -1) {
    type === 'req' && pendingAjax[index].c(FAST_CLICK_MSG)
    pendingAjax.splice(index, 1)
  }
}

// Add a request interceptor
axios.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    const url = config.url
    removePendingAjax(url, 'req')
    config.cancelToken = new CancelToken(c => {
      pendingAjax.push({
        url,
        c
      })
    })
    return config
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error)
  }
)

// Add a response interceptor
axios.interceptors.response.use(
  function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    removePendingAjax(response.config.url, 'resp')
    return new Promise((resolve, reject) => {
      if (+response.data.code !== 0) {
        reject(new Error('network error:' + response.data.msg))
      } else {
        resolve(response)
      }
    })
  },
  function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    Message.error(error)
    return Promise.reject(error)
  }
)
複製代碼

每次執行request攔截器,判斷pendingAjax數組中是否還存在一樣的url。若是存在,則刪除數組中的這個api而且執行數組中在pending的ajax請求的cancel函數進行請求取消,而後就正常發送第二次的ajax請求而且將該api添加到數組中。等請求完結後刪除數組中的這個api

let pendingAjax = []
const FAST_CLICK_MSG = '數據請求中,請稍後'
const CancelToken = axios.CancelToken
const removePendingAjax = (config, c) => {
  const url = config.url
  const index = pendingAjax.findIndex(i => i === url)
  if (index > -1) {
    c ? c(FAST_CLICK_MSG) : pendingAjax.splice(index, 1)
  } else {
    c && pendingAjax.push(url)
  }
}

// Add a request interceptor
axios.interceptors.request.use(
  function (config) {
    // Do something before request is sent
    config.cancelToken = new CancelToken(c => {
      removePendingAjax(config, c)
    })
    return config
  },
  function (error) {
    // Do something with request error
    return Promise.reject(error)
  }
)

// Add a response interceptor
axios.interceptors.response.use(
  function (response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    removePendingAjax(response.config)
    return new Promise((resolve, reject) => {
      if (+response.data.code !== 0) {
        reject(new Error('network error:' + response.data.msg))
      } else {
        resolve(response)
      }
    })
  },
  function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error
    if (error.message !== FAST_CLICK_MSG) { 
      // 修復 由 網絡超時等緣由,致使 當前請求 url 未從 pendingReqs 刪除 
      pendingAjax.splice(0, pendingAjax.length) 
      Message.error('網絡開小差中') 
      return Promise.reject(error) 
    } 
  }
)
複製代碼

每次執行request攔截器,判斷pendingAjax數組中是否還存在一樣的url。若是存在,則執行自身的cancel函數進行請求攔截,不重複發送請求,不存在就正常發送而且將該api添加到數組中。等請求完結後刪除數組中的這個api

總結

axios 是基於 XMLHttpRequest 的封裝,針對 fetch ,也有相似的解決方案 AbortSignal 查看詳情。你們能夠針對各自的項目進行選取

相關文章
相關標籤/搜索