🌗 实现取消功能
# 需求分析
在有些场景,希望能 主动取消请求。
例如常见的搜索框案例,用户在输入过程中,输入框的内容不断变化。正常情况每次变化都应该向服务器发送一次请求。但是当用户输入过快的时候,不希望每次变化请求都会发送出去。通常解决方案是使用 防抖 延时发送请求。
但是有一种极端情况是,后段接口很慢,比如超过 1s 才会响应,这个时候及时做了 200ms 的防抖,在慢慢输入(每次输入间隔超过 200ms)的情况下,在前面的请求没有响应之前,也有可能发出多个请求。因为接口的响应时长不定,如果先发出去的请求响应时长比后发出去的请求要就,那么后请求的响应先回来,先请求的响应后回来,就会出现前面请求响应结果覆盖后面请求响应结果的情况,那么造成响应结果混乱。在这个场景下,除了做防抖,还希望后面的请求发送时,如果前面的请求还没有响应, 可以把前面的请求取消。
从 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.');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
给
axios
添加一个CancelToken
对象,它有一个source
方法返回一个source
对象,source.token
是每一次请求的时候传给配置对象中的cancelToken
属性,然后在请求中发出去后,可以通过cancelToken
取消请求。
或者支持另一种方式的调用:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
cancel()
2
3
4
5
6
7
8
9
10
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) }) }
1
2
3
4
5
6
7
# CancelToken
类的实现
# 接口定义
src/types/index.ts
:
export interface AxiosRequestConfig {
// ...
cancelToken?: CancelToken
}
2
3
4
export interface CancelToken {
promise: Promise<string>
reason?: string
}
export interface Canceler {
(message?: string): void
}
export interface CancelExecutor {
(cancel: Canceler): void
}
2
3
4
5
6
7
8
9
10
11
12
CancelToken
是实例类型的接口定义,Canceler
是取消方法的接口定义,CancelExecutor
是CancelToken
类构造函数参数的接口定义。
# 代码实现
创建单独的目录管理 cancel
相关的代码。
创建文件 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)
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在
CancelToken
构造函数内部,实例化一个pending
状态的 Promise 对象,然后用一个resolvePromise
变量指向resolve
函数,接着执行executor
函数,传入一个cancel
函数,在cancel
函数内部,调用resolvePromise
把 Promise 对象 从pending
变为resolved
状态。
接着在 src/core/xhr.ts
中添加取消请求的逻辑:
const { data = null, url, method = 'get', headers, responseType, timeout, cancelToken} = config
加入从
config
中获取cancelToken
。
if (cancelToken) {
cancelToken.promise.then(reason => {
request.abort()
reject(reason)
})
}
2
3
4
5
6
# CancelToken
拓展静态接口
# 接口定义
在 src/types/index.ts
中:
export interface CancelTokenSource {
token: CancelToken
cancel: Canceler
}
export interface CancelTokenStatic{
new(executor: CancelExecutor): CancelToken
source(): CancelTokenSource
}
2
3
4
5
6
7
8
9
10
其中
CancelTokenSource
作为CancelToken
类静态方法source
函数的返回值类型,CancelTokenStatic
作为CancelToken
类的类类型。
# 代码实现
在 src/cancel/CancelToken.ts
中:
export default class CancelToken {
// ...
static source(): CancelTokenSource {
let cancel!: Canceler
const token = new CancelToken(c => {
cancel = c
})
return {
cancel,
token
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在类中,添加一个
source
静态方法,定义一个cancel
变量实例化一个CancelToken
类型的对象,然后在executor
函数中,把cancel
指向参数c
这个取消函数。这样满足了需求分析中第一种使用方式,但是第一种使用方式的例子中,在捕获请求的时候,通过
axios.isCancel
来判断错误参数e
是不是一次取消请求导致的错误。要对取消错误的原因做一层包装,并且给axios
拓展静态方法。
# Cancel
类的实现以及 axios
的拓展
# 接口定义
export interface Cancel {
message?: string
}
export interface CancelStatic {
new(message?: string): Cancel
}
2
3
4
5
6
7
export interface AxiosStatic extends AxiosInstance {
create(config?: AxiosRequestConfig): AxiosInstance
CancelToken: CancelTokenStatic
Cancel: CancelStatic
isCancel: (value: any) => boolean
}
2
3
4
5
6
7
其中
Cancel
是实例类型的接口定义;CancelStatic
是类类型的接口定义,并且要给axios
拓展了多个静态方法。
# 代码实现
创建 src/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
}
2
3
4
5
6
7
8
9
10
11
Cancel
类拥有一个message
公共属性。isCancel
方法通过insanceof
来判断传入的值是不是一个Cancel
对象。
然后对 CancelToken
类中的 reason
类型做修改,把它变成一个 Cancel
类型的实例。
修改定义: src/types/index.ts
:
export interface CancelToken {
promise: Promise<Cancel>
reason?: Cancel
}
2
3
4
然后修改实现部分, src/cancel/CancelToken.ts
:
interface ResolvePromise {
(reason?: Cancel): void
}
2
3
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)
})
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
然后给 aixos
拓展静态方法,提供用户使用:
src/axios
:
import CancelToken from './cancel/CancelToken'
import Cancel, { isCancel } from './cancel/Cancel'
axios.CancelToken = CancelToken
axios.Cancel = Cancel
axios.isCancel = isCancel
2
3
4
5
6
# 额外逻辑实现
除此之外,还需要实现一些额外逻辑,比如当一个请求携带的 cancelToken
已经被使用过,那么甚至都可以不发送这个请求,直接跑出一个异常即可,并且抛异常的信息就是取消的原因,所以需要给 CancelToken
拓展一个方法。
首先修改定义部分。
src/types/index.ts
:
export interface CancelToken {
promise: Promise<Cancel>
reason?: Cancel
throwIfRequested(): void
}
2
3
4
5
6
然后实现这个方法:
src/cancel/CancelToken.ts
:
export default class CancelToken {
// ...
throwIfRequested():void {
if(this.reason) throw this.reason
}
// ...
}
2
3
4
5
6
7
8
9
判断如果存在
this.reason
,说明这个token
已经被使用过了,直接抛出错误即可。
接下来在发送请求之前增加一段逻辑:
src/core/dispatchRequest.ts
:
export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
throwIfCancellationRequested(config)
processConfig(config)
// ...
}
function throwIfCancellationRequested(config: AxiosRequestConfig): void {
if (config.cancelToken) {
config.cancelToken.throwIfRequested()
}
}
2
3
4
5
6
7
8
9
10
11
12
发送请求之前,需要检查一下配置的
cancelToken
是否被使用过了,如果被使用过就不用发送请求,直接抛出异常即可。
# 编写测试 DEMO
examples/server.js
添加 router
:
router.get('/cancel/get',function(req,res){
setTimeout(()=>{
res.json('hello')
},1000)
})
router.post('/cancel/post',function(req,res){
setTimeout(()=>{
res.json(req.body)
},1000)
})
2
3
4
5
6
7
8
9
10
11
app.ts
:
import axios, { Canceler } from '../../src'
const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios.get('/cancel/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('/cancel/post', { a: 1 }, { cancelToken: source.token }).catch(function(e) {
if (axios.isCancel(e)) {
console.log(e.message)
}
})
}, 100)
let cancel: Canceler
axios.get('/cancel/get', {
cancelToken: new CancelToken(c => {
cancel = c
})
}).catch(function(e) {
if (axios.isCancel(e)) {
console.log('Request canceled')
}
})
setTimeout(() => {
cancel()
}, 200)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40