需求
有些场景下,我们希望能主动取消请求,比如常见的搜索框案例,在用户输入过程中,搜索框的内容也在不断变化,正常情况每次变化我们都应该像服务端发送一次请求。但是当用户输入过快的时候,我们不希望每次变化都发请求出去,通常一个解决方案是前端用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实现了异步分离。接下来我们补充完善其他功能。