# [Axios] 如何 Cancel 已經發出的 Request?

最近收到一個很有趣的需求:希望我們取消已經發出但是未獲得 Response 的 Request 。

因此基於圓滿完成任務後,必須留下痕 (紀) 跡 (錄) 的精神,決定來寫一下這篇。

# CancelToken

Axios 在 v0.15 版本之後就已經支援了基於 cancelable promise proposal 的 Cancelable 功能,因此目前我們使用的 Axios 應該都擁有這項能力才是。

查找 Axios 的 Document 後,了解其 cancel request 的思路其實也很單純。

  1. 使用 CancelToken.source ( ) 產生 cancel instance,其中包含一個 unique token。
  2. 當 axios 要 send request 前,我們就在 option 中加上這個 token 作為 axios 的識別。
  3. 當我們要 cancel 的時候,僅需要利用 source.cancel() 即可達到目標。

p.s. 一個 cancel token 可以 cancel 多個 request.

以 github 上 axios 的範例來說,程式碼可以寫成這樣。

// 生成 CancelToken 的 instance 命名為 source.
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
  cancelToken: source.token // 將 source 的 unique token 加入 request,作為 cancel 的識別
})
.catch(function (thrown) {
  // 如果 request 被啟動 cancel ,則做一些 canceled 的處理。
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});
//post 一樣可以進行 cancel ,只是 options 變成放在第三個位置。
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.');

#interceptors 中使用 CancelToken

在實務上,我們通常會先使用 axios create 一個客製化的 instance,再用 instance.interceptors.request / instance.interceptors.response 攔截 ajax 的 request, response

這種情況下,我們該怎麼做才能將 cancelToken 加入至生成的 instance 呢?

依照 Document 中的範例:

我們可以 callback function 的方式將 cancel function reference 出來。

所以我們就可以將原本的 instance 改成以下的寫法。

import axios from 'axios';
const CancelToken = axios.CancelToken;
let cancelFunc;
axios.interceptors.request.use((config) => {
  if (cancelFunc) cancelFunc(); // cancel request
  // generate the cancel token instance to the request.
  // And reference the cancelFunc to the callback function `c`.
  config.cancelToken =  new CancelToken(function executor(c) {
    cancelFunc = c;
  })
  return config
}, function (error) {
  return Promise.reject(error)
});

甚至再進階一點,我們創建一個 request pool 蒐集所有的 request ,使其每當有相同的 request 被重複發送,就 cancel 掉上一個未回來的 request。

// 引用 axios 的 cancelToken
const CancelToken = axios.CancelToken
// 建立一個 request pool
const requestPool = /** @type { {[url: string]: import('axios').Canceler } } */ ({})
// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 300000 // request timeout
})
// request interceptor
service.interceptors.request.use(config => {
    //cancel 掉 request pool 中相同的 requst.
    //hint. 若 pool 中的 request 上已經 response, cancel 的 call 不會生效。
    if (requestPool[config.url]) {
      const cancel = requestPool[config.url]
      delete requestPool[config.url]
      cancel(JSON.stringify({ status: 'cancel', message: 'Cancel repeated request:' + config.url }))
    }
    // 將新的 request 的 cancel 覆蓋掉舊的 cancel request.
    config.cancelToken = new CancelToken(function executor (c) {
      requestPool[config.url] = c
    })
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)
// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
   */
  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    return response.data
  },
  error => {
    if (!IsJsonString(error.message)) {
      return Promise.reject(error)
    }
    const errMsg = JSON.parse(error.message)
    if (errMsg.status === 'cancel') return console.warn(errMsg.message)
  }
)

接下來我們就可以來嘗試看看是否真的有生效 (cancel 掉前面的 request)

至此,我們就完美達成了需求啦~!

可喜可賀! 可喜可賀!

Like z20240z's work