7、ts-axios 取消功能

需求

有些場(chǎng)景下叮阅,我們希望能主動(dòng)取消請(qǐng)求刁品,比如常見的搜索框案例,在用戶輸入過(guò)程中浩姥,搜索框的內(nèi)容也在不斷變化挑随,正常情況每次變化我們都應(yīng)該像服務(wù)端發(fā)送一次請(qǐng)求。但是當(dāng)用戶輸入過(guò)快的時(shí)候勒叠,我們不希望每次變化都發(fā)請(qǐng)求出去兜挨,通常一個(gè)解決方案是前端用debounce的方案,比如延時(shí)200ms發(fā)送請(qǐng)求眯分。這樣當(dāng)用戶連續(xù)輸入字符時(shí)拌汇,只要輸入間隔小于200ms,前面輸入的字符串都不會(huì)發(fā)請(qǐng)求弊决。

但是還有一種極端情況時(shí)后端接口很慢噪舀,比如超過(guò)1s才能響應(yīng),這個(gè)時(shí)候即使做了200ms的debounce,但是在慢慢輸入(每個(gè)輸入間隔超過(guò)200ms)的情況下与倡,在前面的請(qǐng)求沒有響應(yīng)前先改,也有可能發(fā)出去多個(gè)請(qǐng)求,因?yàn)榻涌诘捻憫?yīng)時(shí)長(zhǎng)是不定的蒸走,如果先發(fā)出去的請(qǐng)求時(shí)長(zhǎng)比后發(fā)出去的請(qǐng)求要就久一些,后請(qǐng)求的響應(yīng)就會(huì)先回來(lái)貌嫡,先請(qǐng)求的響應(yīng)后回來(lái)比驻,就會(huì)出現(xiàn)前面請(qǐng)求的響應(yīng)結(jié)果覆蓋后買呢請(qǐng)求響應(yīng)結(jié)果的情況,那么就亂了岛抄。因此在這個(gè)場(chǎng)景下别惦,我們除了做debounce,還希望后面的請(qǐng)求發(fā)出去的時(shí)候夫椭,如果前面的請(qǐng)求還沒有響應(yīng)掸掸,我們可以把前面的請(qǐng)求取消。

從axios的取消接口設(shè)計(jì)層面蹭秋,我們希望做如下設(shè)計(jì):

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

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (e) {
  if (axios.isCancel(e)) {
    console.log('Request canceled', e.message);
  } else {
    // 處理錯(cuò)誤
  }
});

// 取消請(qǐng)求 (請(qǐng)求原因是可選的)
source.cancel('Operation canceled by the user.');

我們給axios添加一個(gè)CancelToken的對(duì)象扰付,它有一個(gè)source方法可以返回一個(gè)source對(duì)象,source.token是在每次請(qǐng)求的時(shí)候傳給配置對(duì)象中的cancelToken屬性仁讨,然后在請(qǐng)求發(fā)出去之后羽莺,我們可以通過(guò)source.cancel方法取消請(qǐng)求。

我們還支持另一種方法的調(diào)用:

const CancelToken = axios.CancelToken;
let cancel;

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

// 取消請(qǐng)求
cancel();

axios.CancelToken是一個(gè)類洞豁,我們直接把它的實(shí)例化對(duì)象傳給請(qǐng)求配置中的cancelToken屬性盐固,CancelToken的構(gòu)造函數(shù)參數(shù)支持傳入一個(gè)executor方法,該方法的參數(shù)是一個(gè)取消函數(shù)c丈挟,我們可以在executor方法執(zhí)行的內(nèi)部拿到這個(gè)取消函數(shù)c刁卜,賦值給我們外部定義的cancel變量,之后我們可以通過(guò)調(diào)用這個(gè)cancel方法來(lái)取消請(qǐng)求曙咽。

異步分離的設(shè)計(jì)方案

通過(guò)需求分析蛔趴,我們知道,想要實(shí)現(xiàn)取消某次請(qǐng)求桐绒,我們需要為該請(qǐng)求配置一個(gè)cancelToken夺脾,然后在外部調(diào)用一個(gè)cancel方法。

請(qǐng)求的發(fā)送是一個(gè)異步的過(guò)程茉继,最終會(huì)執(zhí)行xhr.send方法咧叭。xhr對(duì)象提供了abort方法,可以把請(qǐng)求取消掉烁竭。因?yàn)槲覀冊(cè)谕獠渴桥霾坏絰hr對(duì)象的菲茬,所以我們想要在執(zhí)行cancel的時(shí)候,去執(zhí)行xhr.abort方法。

現(xiàn)在就相當(dāng)于我們?cè)趚hr異步請(qǐng)求的過(guò)程中婉弹,插入一段代碼睬魂,當(dāng)我們?cè)谕獠繄?zhí)行cancel的時(shí)候,會(huì)驅(qū)動(dòng)這段代碼的執(zhí)行镀赌,然后執(zhí)行xhr.abort取消請(qǐng)求氯哮。

我們可以用Promise來(lái)實(shí)現(xiàn)異步分離,也就是在cancelToken中保存一個(gè)pending狀態(tài)的promise對(duì)象商佛,然后當(dāng)我們執(zhí)行cancel的時(shí)候喉钢,能夠訪問(wèn)到這個(gè)promise對(duì)象,把它從pending狀態(tài)變成resolved狀態(tài)良姆,這樣我們就可以在then函數(shù)中去實(shí)現(xiàn)取消請(qǐng)求的邏輯肠虽,類似如下:

if (cancelToken) {
  cancelToken.promise
    .then(reason => {
      request.abort()
      reject(reason)
    })
}
CancelToken類實(shí)現(xiàn)
  • 接口定義
interface AxiosRequestConfig {
  // ...
  cancelToken?: CancelToken,
}
interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance
  CancelToken: CancelTokenStatic
}
interface CancelToken {
  promise: Promise<string>
  reason?: string
}

interface Canceler {
  (message?: string): void
}

interface CancelExecutor {
  (cancel: Canceler): void
}
interface CancelTokenStatic {
  new(executor: CancelExecutor): CancelToken
}

cancel/cancelToken.ts

import { CancelExecutor } from "../types"

interface ResolvePromise {
  (reason?: string): void
}

export default class CancelToken {
  promise: Promise<string>
  reason?: string
  constructor(executor: CancelExecutor) {
    let resolvePromise: ResolvePromise
    this.promise = new Promise<string>(resolve => {
      resolvePromise = resolve
    })
    executor(message => {
      if (this.reason) {
        return
      }
      this.reason = message
      resolvePromise(this.reason)
    })
  }
}

修改xhr.ts

if (cancelToken) {
  cancelToken.promise.then(reason => {
    request.abort()
    reject(reason)
  })
}

axios.ts

// ...
axios.CancelToken = CancelToken

demo

import axios, { Canceler } from '../../src/index'
const CancelToken = axios.CancelToken
let cancel: Canceler

axios.get('/api/extend/get', {
  cancelToken: new CancelToken(c => {
    cancel = c
  })
}).catch(function(e) {
  console.log('Request canceled')
})

setTimeout(() => {
  cancel()
}, 200)

這樣就實(shí)現(xiàn)了第二種用法,接著我們要實(shí)現(xiàn)第一種使用方法玛追,那我們就需要給CancelToken擴(kuò)展靜態(tài)接口税课。

CancelToken擴(kuò)展靜態(tài)接口
  • 定義接口
interface CancelTokenStatic {
  new(executor: CancelExecutor): CancelToken
  source(): CancelTokenSource
}

interface CancelTokenSource {
  token: CancelToken,
  cancel: Canceler
}

修改cancel/cancelToken.ts

export default class CancelToken {
   // ...
  static source(): CancelTokenSource {
    let cancel!: Canceler
    const token = new CancelToken(c => {
      cancel = c
    })
    return {
      cancel,
      token
    }
  }
}

source靜態(tài)方法,就是在被調(diào)用的時(shí)候痊剖,實(shí)例化一個(gè)CancelToken的對(duì)象韩玩,然后在executor函數(shù)中,把cancel指向參數(shù)c這個(gè)取消函數(shù)陆馁。

這樣就滿足了我們的第一種使用方式啸如,但是在第一種使用方式的例子中,我們?cè)谘a(bǔ)貨請(qǐng)求的時(shí)候氮惯,通過(guò)axios.isCancel來(lái)判斷這個(gè)錯(cuò)誤e是不是一次取消請(qǐng)求導(dǎo)致的錯(cuò)誤叮雳,接下來(lái)我們對(duì)取消請(qǐng)求的原因做一層包裝,并且給axios擴(kuò)展靜態(tài)方法妇汗。
Cancel類實(shí)現(xiàn)及axios擴(kuò)展

  • 接口定義
interface Cancel {
  message?: string
}

interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance
  CancelToken: CancelTokenStatic
  isCancel: (val: any) => boolean
}

cancel/cancel.ts

export default class Cancel {
  message?: string
  constructor(message?: string) {
    this.message = message
  }
}

export function isCancel(value: any): boolean {
  return value instanceof Cancel
}

Cancel類型擁有一個(gè)message屬性帘不。isCancel通過(guò)instanceof來(lái)判斷傳入的值是不是一個(gè)Cancel對(duì)象。
接著杨箭,我們對(duì)CancelToken類中的reason類型做修改寞焙,把它變成Cancel類型的實(shí)例。
修改定義部分

interface CancelToken {
  promise: Promise<Cancel>
  reason?: Cancel
}

修改實(shí)現(xiàn)部分

import { CancelExecutor, CancelTokenSource, Canceler } from "../types"
import Cancel from "./cancel"

interface ResolvePromise {
  (reason?: Cancel): void
}

export default class CancelToken {
  promise: Promise<Cancel>
  reason?: Cancel
  constructor(executor: CancelExecutor) {
    let resolvePromise: ResolvePromise
    this.promise = new Promise<Cancel>(resolve => {
      resolvePromise = resolve
    })
    executor(message => {
      if (this.reason) {
        return
      }
      this.reason = new Cancel(message)
      resolvePromise(this.reason)
    })
  }
  static source(): CancelTokenSource {
    let cancel!: Canceler
    const token = new CancelToken(c => {
      cancel = c
    })
    return {
      cancel,
      token
    }
  }
}

然后修改axios互婿,添加靜態(tài)方法

// ...
axios.isCancel = isCancel
額外邏輯實(shí)現(xiàn)

除此以外捣郊,我們還需要實(shí)現(xiàn)一些額外邏輯,比如當(dāng)一個(gè)請(qǐng)求攜帶的cancelToken已經(jīng)使用過(guò)慈参,那么我們甚至可以不發(fā)送這個(gè)請(qǐng)求呛牲,只需要拋出一個(gè)異常即可,并且拋異常的信息就是我們?nèi)∠脑蛲耘洌晕覀冃枰oCancelToken擴(kuò)展一個(gè)方法娘扩。
先修改定義部分:

interface CancelToken {
  promise: Promise<Cancel>
  reason?: Cancel
  throwIfRequested(): void
}

修改實(shí)現(xiàn)部分:

import { CancelExecutor, CancelTokenSource, Canceler } from "../types"
import Cancel from "./cancel"

interface ResolvePromise {
  (reason?: Cancel): void
}

export default class CancelToken {
  // ...
  throwIfRequested(): void {
    if (this.reason) {
      throw this.reason
    }
  }
}

如果有reason着茸,說(shuō)明這個(gè)token已經(jīng)使用過(guò)了,直接拋錯(cuò)琐旁。

接下來(lái)在發(fā)送請(qǐng)求前涮阔,添加一段邏輯:

export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
  throwIfCancellationRequested(config)
  processConfig(config)
  return xhr(config)
}
function throwIfCancellationRequested(config: AxiosRequestConfig): void {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested()
  }
}
demo
import axios, { Canceler } from '../../src/index'

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

axios.get('/api/extend/get', {
  cancelToken: source.token
}).catch(function(e) {
  if (axios.isCancel(e)) {
    console.log('Request canceled', e.message)
  }
})

setTimeout(() => {
  source.cancel('Operation canceled by the user.')

  axios.post('/api/extend/post', { a: 1 }, { cancelToken: source.token }).catch(function(e) {
    if (axios.isCancel(e)) {
      console.log(e.message)
    }
  })
}, 100)
let cancel: Canceler

axios.get('/api/extend/get', {
  cancelToken: new CancelToken(c => {
    cancel = c
  })
}).catch(function(e) {
  console.log('Request canceled')
})

setTimeout(() => {
  cancel()
}, 200)

從demo可以看出,雖然我們發(fā)送了3個(gè)請(qǐng)求灰殴,但是實(shí)際只發(fā)出了2個(gè)敬特,因?yàn)榈诙€(gè)在發(fā)送之前,檢測(cè)到已經(jīng)執(zhí)行過(guò)取消操作牺陶,所以直接拋錯(cuò)擅羞,沒有發(fā)送。

至此义图,我們完成了ts-axios的請(qǐng)求取消功能,我們巧妙地利用了promise實(shí)現(xiàn)了異步分離召烂。接下來(lái)我們補(bǔ)充完善其他功能碱工。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市奏夫,隨后出現(xiàn)的幾起案子怕篷,更是在濱河造成了極大的恐慌,老刑警劉巖酗昼,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件廊谓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡麻削,警方通過(guò)查閱死者的電腦和手機(jī)蒸痹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)呛哟,“玉大人叠荠,你說(shuō)我怎么就攤上這事∩ㄔ穑” “怎么了榛鼎?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鳖孤。 經(jīng)常有香客問(wèn)我者娱,道長(zhǎng),這世上最難降的妖魔是什么苏揣? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任黄鳍,我火速辦了婚禮,結(jié)果婚禮上平匈,老公的妹妹穿的比我還像新娘际起。我一直安慰自己拾碌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布街望。 她就那樣靜靜地躺著校翔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灾前。 梳的紋絲不亂的頭發(fā)上防症,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音哎甲,去河邊找鬼蔫敲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛炭玫,可吹牛的內(nèi)容都是我干的奈嘿。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼裙犹,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了衔憨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤践图,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后码党,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體德崭,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡接癌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了扣讼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡椭符,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出销钝,到底是詐尸還是另有隱情有咨,我是刑警寧澤蒸健,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布婉商,位于F島的核電站,受9級(jí)特大地震影響渣叛,放射性物質(zhì)發(fā)生泄漏丈秩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一淳衙、第九天 我趴在偏房一處隱蔽的房頂上張望蘑秽。 院中可真熱鬧,春花似錦箫攀、人聲如沸肠牲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)缀雳。三九已至,卻和暖如春梢睛,著一層夾襖步出監(jiān)牢的瞬間肥印,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工扬绪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裤唠。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓挤牛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親种蘸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子墓赴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 概述 在前端開發(fā)過(guò)程中,我們經(jīng)常會(huì)遇到需要發(fā)送異步請(qǐng)求的情況航瞭。而使用一個(gè)功能齊全诫硕,接口完善的HTTP請(qǐng)求庫(kù),能夠在...
    grain先森閱讀 1,577評(píng)論 0 4
  • Axios是近幾年非晨睿火的HTTP請(qǐng)求庫(kù)章办,官網(wǎng)上介紹Axios 是一個(gè)基于 promise 的 HTTP 庫(kù),可以...
    milletmi閱讀 3,506評(píng)論 0 9
  • 后來(lái)的我們什么都有了滨彻,卻沒有了“我們”藕届。這是電影《最后的我們》里一句非常經(jīng)典的臺(tái)詞。至今仍記得亭饵,那個(gè)冬日的...
    喬瑾源閱讀 388評(píng)論 0 8
  • ta要請(qǐng)禾瘋子吃飯词顾,禾瘋子婉拒并致謝過(guò)。一來(lái)真忙碱妆,二來(lái)禾瘋子吃飯有個(gè)壞習(xí)慣肉盹。不需要理由可以拒絕的不吃,基本素質(zhì)不行...
    禾傷閱讀 144評(píng)論 0 0
  • 昨天第一次畫的 今天修改的兩個(gè) 今天新畫的兩個(gè)^_^
    F56春雨sunny閱讀 515評(píng)論 1 1