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实现了异步分离。接下来我们补充完善其他功能。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容