天天看點

Axios 如何取消請求?

作者:活潑小陳視訊剪輯

應用場景

取消請求在前端有時候會用到,以下是兩個工作中可能會用到的場景

  1. tab切換時重新整理某個清單資料,如果他們共用一個變量存儲資料清單,當請求有延時,可能會導緻兩個tab資料錯亂;
  2. 導出檔案或下載下傳檔案時,中途取消 。

如何取消請求

取消http請求,axios文檔裡提供了兩種用法:

第一種:使用 CancelToken

const { CancelToken, isCanCel } = axios;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(thrown => {
  if (isCancel(thrown)) {
    // 擷取 取消請求 的相關資訊
    console.log('Request canceled', thrown.message);
  } else {
    // 處理其他異常
  }
});

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

// 取消請求。參數是可選的,參數傳遞一個取消請求的相關資訊,在 catch 鈎子函數裡能擷取到
source.cancel('Operation canceled by the user.');
           

第二種:給構造函數 CancelToken 傳遞一個 executor 函數作為參數。這種方法的好處是,可以用同一個 cancel token 來取消多個請求

const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // 參數 c 也是個函數
    cancel = c;
  })
});
// 取消請求,參數用法同上
cancel();
           

項目中用法示例

在一個真實的項目中,一般都會對axios進行二次封裝,針對請求、響應、狀态碼、code等做處理。貼一個項目裡常用的request.js:

import axios from 'axios'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 建立一個 axios 執行個體,并改變預設配置
const service = axios.create({
  baseURL: process.env.BASE_API, // api 的 base_url
  timeout: 5000 // request timeout
})

// 請求攔截
service.interceptors.request.use(
  config => {
    // Do something before request is sent
    if (store.getters.token) {
      // 讓每個請求攜帶token-- ['X-Token']為自定義key 請根據實際情況自行修改
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // Do something with request error
    console.log(error) // for debug
    Promise.reject(error)
  }
)

// 響應攔截
service.interceptors.response.use(
  response => response,
  error => {
    alert(error)
    return Promise.reject(error)
  }
)

export default service
           

對于某一個請求添加取消的功能,要在調用api時,加上cancelToken選項,使用時的示例:

// api.js
import request from 'request'
export function getUsers(page, options) {
  return request({
    url: 'api/users',
    params: {
      page
    },
    ...options
  })
}
           
// User.vue
import { CancelToken, isCancel } from 'axios'
import { getUsers } from 'api'

...

cancel: null

...

toCancel() {
  this.cancel('取消請求')
}

getUsers(1,
  {
    cancelToken:  new CancelToken(c => (this.cancel = c))
  }
)
.then(...)
.catch(err => {
  if (isCancel) {
    console.log(err.message)
  } else {
    ...
  }
})
           

以上,我們就可以順順利利地使用封裝過的axios,取消某一個請求了。其原理無非就是把cancelToken的配置項,在調用api時加上,然後就可以在業務代碼取消特定請求了。

批量取消請求

在 document 裡的第二種方法已經說過:通過指定同一個cancel token來取消。但是,在上面的項目示例中,不能控制拿到相同的cancel token。我們可以換個思路:用數組儲存每個需要取消的cancel token,然後逐一調用數組裡的每一項即可:

// User.vue
import { CancelToken, isCancel } from 'axios'
import { getUsers } from 'api'

...

cancel: []

...

toCancel() {
  while (this.cancel.length > 0) {
    this.cancel.pop()('取消請求')
  }
}

getUser1(1,
  {
    cancelToken:  new CancelToken(c1 => (this.cancel.push(c1)))
  }
)

getUser2(2,
 {
  cancelTokem: new CancleTokem(c2 => (this.cancel.push(c2)))
 }
)
           

切換路由時,取消請求

上面講了取消一個請求及頁面内批量abort的方法,此外,還有一種需求——切換路由時,取消所有。 這裡不詳細贅述了,大概思路就是在請求攔截器裡,統一加個token,并設定全局變量source控制一個cancel token,在路由變化時調用cancel方法。

http.interceptors.request.use(config => {
    config.cancelToken = store.source.token
    return config
}, err => {
    return Promise.reject(err)
})

router.beforeEach((to, from, next) => {
    const CancelToken = axios.CancelToken
    store.source.cancel && store.source.cancel()
    store.source = CancelToken.source()
    next()
})

// 全局變量
store = {
    source: {
        token: null,
        cancel: null
  }
}
           

取消請求的實作原理

cancelToken的source方法維護了一個對象,裡面包括了token令牌和cancel方法,token來自與構造函數CancelToken,調用cancel方法後,token的promise狀态為resolved,進而又調用了xhr的abort方法,取消請求成功。 來分析下取消請求是怎麼實作的,先從一個簡單的取消請求的例子開始:

var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/get?name=xmz', {
    cancelToken : source.token
}).then((response)=>{
    console.log('response', response)
}).catch((error)=>{
    if(axios.isCancel(error)){
        console.log('取消請求傳遞的消息', error.message)
    }else{
        console.log('error', error)
    }
})
// 取消請求
source.cancel('取消請求傳遞這條消息');
           

這就是一個簡單的取消請求的例子,那麼就從最開始的axios.CancelToken來看,先去axios/lib/axios.js檔案中。

axios.CancelToken = require('./cancel/CancelToken');
           

不費吹灰之力,就找到了CancelToken,在例子中我們調用了source方法,那麼就去axios/lib/cancel/CancelToken.js檔案中看看這個source方法到底是幹什麼的?

CancelToken.source = function(){
    var cancel;
    var token = new CancelToken(function executor(c) {
        cancel = c
    })
    return {
        token : token,
        cancel : cancel
    }
}
           

source方法很簡單,就是傳回一個具有token和cancel屬性的對象,但是token和cancel都是通過CancelToken這個構造函數來的,那麼還在這個檔案中向上看,找到CancelToken函數。

function CancelToken (executor){
    // ...
    // 判斷executor是一個函數,不然就報錯
    var resolvePromise;
    this.promise = new Promise(function(resolve){
        resolvePromise = resolve;
    })
    var token = this;
    // 以上token現在有一個promise屬性,是一個未成功的promise對象;
    executor(function cancel(message){
        if(token.reason){
            return;
        }
        token.reason = new Cancel(message);
        resolvePromise(token.reason);
    })
    // 這個cancel函數就是 上面函數中的cancel,也就是source.cancel;
}
           

現在知道了source.cancel是一個函數,souce.token是一個執行個體化對象,暫時就知道這些,繼續看文章最開始的例子,接下來是去發送請求了,最下面還有一行代碼是執行souce.cancel(); souce.cancel就是用來觸發取消請求的函數。 現在再回頭來看,上面的cancel函數,cancel執行,給token加了一個reason屬性,那麼看下這個reason屬性是什麼吧,看下這個Cancel構造函數,在axios/lib/cancel/Cancel.js檔案中

function Cancel(message){
    this.message = message
}
           

Cancel特别簡單就是給執行個體化對象添加一個message屬性,是以現在token.reason是一個具有message屬性的對象了。 繼續回到cancel函數中,resolvePromise函數執行了,那麼token.promise對象,這個原本未變成,成功狀态的promise,變成了成功狀态了,并且将token.reason對象傳遞過去了。 簡單總結一下,執行取消函數,就是讓token的promise的狀态變成了成功; 好了,突然發現分析中斷了,變成成功狀态又怎樣了,怎麼取消的呢?雖然現在的同步代碼都執行完了,但是請求還沒發送出去呢,我們還要去看發送請求的函數

在分析發送請求之前,再看下最開始的例子,和最普通的發送一個get請求還是有一點差別的,配置對象中多了,一個cancelToken的屬性,值是token,到底起了什麼作用呢,去axios/lib/adapters/xhr.js中一探究竟(這裡隻截取其中關于cancelToken的部分)。

// 在發送請求之前,驗證了cancelToken,看來此處就是用來取消請求的;
if(config.cancelToken){
    // 具體是如何取消的,是在這個判斷内定義的;
    config.cancelToken.promise.then(function(cancel){
        request.abort();
        reject(cancel);
        request = null;
    })
}
// 發送請求
request.send(requestData);
           

仔細看這隻是一個promise的then函數,隻有在promise的狀态變成成功後才會執行,而剛才我們分析了,cancel就是讓這個promise的狀态變成成功,是以如果執行了,取消請求的函數,這個then就會執行,取消發送請求,并且把發送請求的promise變成reject,被axiox.get().catch()捕獲; 流程已經清楚了,最後再總結一下: 執行cancel是讓token的promise變成成功,在真正發送請求之前,驗證token.promise的狀态是否已經變了,如果變了,就取消請求,就是這樣一個簡單的思想來進行取消請求的。

繼續閱讀