7、ts-axios 取消功能

需求

有些场景下,我们希望能主动取消请求,比如常见的搜索框案例,在用户输入过程中,搜索框的内容也在不断变化,正常情况每次变化我们都应该像服务端发送一次请求。但是当用户输入过快的时候,我们不希望每次变化都发请求出去,通常一个解决方案是前端用debounce的方案,比如延时200ms发送请求。这样当用户连续输入字符时,只要输入间隔小于200ms,前面输入的字符串都不会发请求。

但是还有一种极端情况时后端接口很慢,比如超过1s才能响应,这个时候即使做了200ms的debounce,但是在慢慢输入(每个输入间隔超过200ms)的情况下,在前面的请求没有响应前,也有可能发出去多个请求,因为接口的响应时长是不定的,如果先发出去的请求时长比后发出去的请求要就久一些,后请求的响应就会先回来,先请求的响应后回来,就会出现前面请求的响应结果覆盖后买呢请求响应结果的情况,那么就乱了。因此在这个场景下,我们除了做debounce,还希望后面的请求发出去的时候,如果前面的请求还没有响应,我们可以把前面的请求取消。

从axios的取消接口设计层面,我们希望做如下设计:

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 {
    // 处理错误
  }
});

// 取消请求 (请求原因是可选的)
source.cancel('Operation canceled by the user.');

我们给axios添加一个CancelToken的对象,它有一个source方法可以返回一个source对象,source.token是在每次请求的时候传给配置对象中的cancelToken属性,然后在请求发出去之后,我们可以通过source.cancel方法取消请求。

我们还支持另一种方法的调用:

const CancelToken = axios.CancelToken;
let cancel;

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

// 取消请求
cancel();

axios.CancelToken是一个类,我们直接把它的实例化对象传给请求配置中的cancelToken属性,CancelToken的构造函数参数支持传入一个executor方法,该方法的参数是一个取消函数c,我们可以在executor方法执行的内部拿到这个取消函数c,赋值给我们外部定义的cancel变量,之后我们可以通过调用这个cancel方法来取消请求。

异步分离的设计方案

通过需求分析,我们知道,想要实现取消某次请求,我们需要为该请求配置一个cancelToken,然后在外部调用一个cancel方法。

请求的发送是一个异步的过程,最终会执行xhr.send方法。xhr对象提供了abort方法,可以把请求取消掉。因为我们在外部是碰不到xhr对象的,所以我们想要在执行cancel的时候,去执行xhr.abort方法。

现在就相当于我们在xhr异步请求的过程中,插入一段代码,当我们在外部执行cancel的时候,会驱动这段代码的执行,然后执行xhr.abort取消请求。

我们可以用Promise来实现异步分离,也就是在cancelToken中保存一个pending状态的promise对象,然后当我们执行cancel的时候,能够访问到这个promise对象,把它从pending状态变成resolved状态,这样我们就可以在then函数中去实现取消请求的逻辑,类似如下:

if (cancelToken) {
  cancelToken.promise
    .then(reason => {
      request.abort()
      reject(reason)
    })
}
CancelToken类实现
  • 接口定义
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)

这样就实现了第二种用法,接着我们要实现第一种使用方法,那我们就需要给CancelToken扩展静态接口。

CancelToken扩展静态接口
  • 定义接口
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静态方法,就是在被调用的时候,实例化一个CancelToken的对象,然后在executor函数中,把cancel指向参数c这个取消函数。

这样就满足了我们的第一种使用方式,但是在第一种使用方式的例子中,我们在补货请求的时候,通过axios.isCancel来判断这个错误e是不是一次取消请求导致的错误,接下来我们对取消请求的原因做一层包装,并且给axios扩展静态方法。
Cancel类实现及axios扩展

  • 接口定义
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类型拥有一个message属性。isCancel通过instanceof来判断传入的值是不是一个Cancel对象。
接着,我们对CancelToken类中的reason类型做修改,把它变成Cancel类型的实例。
修改定义部分

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

修改实现部分

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,添加静态方法

// ...
axios.isCancel = isCancel
额外逻辑实现

除此以外,我们还需要实现一些额外逻辑,比如当一个请求携带的cancelToken已经使用过,那么我们甚至可以不发送这个请求,只需要抛出一个异常即可,并且抛异常的信息就是我们取消的原因,所以我们需要给CancelToken扩展一个方法。
先修改定义部分:

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

修改实现部分:

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,说明这个token已经使用过了,直接抛错。

接下来在发送请求前,添加一段逻辑:

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可以看出,虽然我们发送了3个请求,但是实际只发出了2个,因为第二个在发送之前,检测到已经执行过取消操作,所以直接抛错,没有发送。

至此,我们完成了ts-axios的请求取消功能,我们巧妙地利用了promise实现了异步分离。接下来我们补充完善其他功能。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容