需求
有些場(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ǔ)充完善其他功能碱工。