axios取消功能的设计与实现
取消功能的設(shè)計(jì)與實(shí)現(xiàn)
#需求分析
有些場景下,我們希望能主動取消請求,比如常見的搜索框案例,在用戶輸入過程中,搜索框的內(nèi)容也在不斷變化,正常情況每次變化我們都應(yīng)該向服務(wù)端發(fā)送一次請求。但是當(dāng)用戶輸入過快的時候,我們不希望每次變化請求都發(fā)出去,通常一個解決方案是前端用 debounce 的方案,比如延時 200ms 發(fā)送請求。這樣當(dāng)用戶連續(xù)輸入的字符,只要輸入間隔小于 200ms,前面輸入的字符都不會發(fā)請求。
但是還有一種極端情況是后端接口很慢,比如超過 1s 才能響應(yīng),這個時候即使做了 200ms 的 debounce,但是在我慢慢輸入(每個輸入間隔超過 200ms)的情況下,在前面的請求沒有響應(yīng)前,也有可能發(fā)出去多個請求。因?yàn)榻涌诘捻憫?yīng)時長是不定的,如果先發(fā)出去的請求響應(yīng)時長比后發(fā)出去的請求要久一些,后請求的響應(yīng)先回來,先請求的響應(yīng)后回來,就會出現(xiàn)前面請求響應(yīng)結(jié)果覆蓋后面請求響應(yīng)結(jié)果的情況,那么就亂了。因此在這個場景下,我們除了做 debounce,還希望后面的請求發(fā)出去的時候,如果前面的請求還沒有響應(yī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 { // 處理錯誤 } }); // 取消請求 (請求原因是可選的) source.cancel('Operation canceled by the user.');我們給?axios?添加一個?CancelToken?的對象,它有一個?source?方法可以返回一個?source?對象,source.token?是在每次請求的時候傳給配置對象中的?cancelToken?屬性,然后在請求發(fā)出去之后,我們可以通過?source.cancel?方法取消請求。
我們還支持另一種方式的調(diào)用:
const CancelToken = axios.CancelToken; let cancel; axios.get('/user/12345', { cancelToken: new CancelToken(function executor(c) { cancel = c; }) }); // 取消請求 cancel();axios.CancelToken?是一個類,我們直接把它實(shí)例化的對象傳給請求配置中的?cancelToken?屬性,CancelToken?的構(gòu)造函數(shù)參數(shù)支持傳入一個?executor?方法,該方法的參數(shù)是一個取消函數(shù)?c,我們可以在?executor?方法執(zhí)行的內(nèi)部拿到這個取消函數(shù)?c,賦值給我們外部定義的?cancel?變量,之后我們可以通過調(diào)用這個?cancel?方法來取消請求。
#異步分離的設(shè)計(jì)方案
通過需求分析,我們知道想要實(shí)現(xiàn)取消某次請求,我們需要為該請求配置一個?cancelToken,然后在外部調(diào)用一個?cancel?方法。
請求的發(fā)送是一個異步過程,最終會執(zhí)行?xhr.send?方法,xhr?對象提供了?abort?方法,可以把請求取消。因?yàn)槲覀冊谕獠渴桥霾坏?xhr?對象的,所以我們想在執(zhí)行?cancel?的時候,去執(zhí)行?xhr.abort?方法。
現(xiàn)在就相當(dāng)于我們在?xhr?異步請求過程中,插入一段代碼,當(dāng)我們在外部執(zhí)行?cancel?函數(shù)的時候,會驅(qū)動這段代碼的執(zhí)行,然后執(zhí)行?xhr.abort?方法取消請求。
我們可以利用 Promise 實(shí)現(xiàn)異步分離,也就是在?cancelToken?中保存一個?pending?狀態(tài)的 Promise 對象,然后當(dāng)我們執(zhí)行?cancel?方法的時候,能夠訪問到這個 Promise 對象,把它從?pending?狀態(tài)變成?resolved?狀態(tài),這樣我們就可以在?then?函數(shù)中去實(shí)現(xiàn)取消請求的邏輯,類似如下的代碼:
if (cancelToken) {cancelToken.promise .then(reason => { request.abort() reject(reason) }) }#CancelToken 類實(shí)現(xiàn)
接下來,我們就來實(shí)現(xiàn)這個?CancelToken?類,先來看一下接口定義:
#接口定義
types/index.ts:
export interface AxiosRequestConfig {// ... cancelToken?: CancelToken } export interface CancelToken { promise: Promise<string> reason?: string } export interface Canceler { (message?: string): void } export interface CancelExecutor { (cancel: Canceler): void }其中?CancelToken?是實(shí)例類型的接口定義,Canceler?是取消方法的接口定義,CancelExecutor?是?CancelToken?類構(gòu)造函數(shù)參數(shù)的接口定義。
#代碼實(shí)現(xiàn)
我們單獨(dú)創(chuàng)建?cancel?目錄來管理取消相關(guān)的代碼,在?cancel?目錄下創(chuàng)建?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) }) } }在?CancelToken?構(gòu)造函數(shù)內(nèi)部,實(shí)例化一個?pending?狀態(tài)的 Promise 對象,然后用一個?resolvePromise?變量指向?resolve?函數(shù)。接著執(zhí)行?executor?函數(shù),傳入一個?cancel?函數(shù),在?cancel?函數(shù)內(nèi)部,會調(diào)用?resolvePromise?把 Promise 對象從?pending?狀態(tài)變?yōu)?resolved?狀態(tài)。
接著我們在?xhr.ts?中插入一段取消請求的邏輯。
core/xhr.ts:
const { /*....*/ cancelToken } = config if (cancelToken) { cancelToken.promise.then(reason => { request.abort() reject(reason) }) }這樣就滿足了第二種使用方式,接著我們要實(shí)現(xiàn)第一種使用方式,給?CancelToken?擴(kuò)展靜態(tài)接口。
#CancelToken 擴(kuò)展靜態(tài)接口
#接口定義
types/index.ts:
export interface CancelTokenSource {token: CancelToken cancel: Canceler } export interface CancelTokenStatic { new(executor: CancelExecutor): CancelToken source(): CancelTokenSource }其中?CancelTokenSource?作為?CancelToken?類靜態(tài)方法?source?函數(shù)的返回值類型,CancelTokenStatic?則作為?CancelToken?類的類類型。
#代碼實(shí)現(xiàn)
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)方法很簡單,定義一個?cancel?變量實(shí)例化一個?CancelToken?類型的對象,然后在?executor?函數(shù)中,把?cancel?指向參數(shù)?c?這個取消函數(shù)。
這樣就滿足了我們第一種使用方式,但是在第一種使用方式的例子中,我們在捕獲請求的時候,通過?axios.isCancel?來判斷這個錯誤參數(shù) e 是不是一次取消請求導(dǎo)致的錯誤,接下來我們對取消錯誤的原因做一層包裝,并且把給?axios?擴(kuò)展靜態(tài)方法
#Cancel 類實(shí)現(xiàn)及 axios 的擴(kuò)展
#接口定義
export interface Cancel {message?: string } export interface CancelStatic { new(message?: string): Cancel } export interface AxiosStatic extends AxiosInstance { create(config?: AxiosRequestConfig): AxiosInstance CancelToken: CancelTokenStatic Cancel: CancelStatic isCancel: (value: any) => boolean }其中?Cancel?是實(shí)例類型的接口定義,CancelStatic?是類類型的接口定義,并且我們給?axios?擴(kuò)展了多個靜態(tài)方法。
#代碼實(shí)現(xiàn)
我在?cancel?目錄下創(chuàng)建?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?類型的實(shí)例。
先修改定義部分。
types/index.ts:
export interface CancelToken {promise: Promise<Cancel> reason?: Cancel }再修改實(shí)現(xiàn)部分:
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) }) } }接下來我們給?axios?擴(kuò)展一些靜態(tài)方法,供用戶使用。
axios.ts:
import CancelToken from './cancel/CancelToken' import Cancel, { isCancel } from './cancel/Cancel' axios.CancelToken = CancelToken axios.Cancel = Cancel axios.isCancel = isCancel#額外邏輯實(shí)現(xiàn)
除此之外,我們還需要實(shí)現(xiàn)一些額外邏輯,比如當(dāng)一個請求攜帶的?cancelToken?已經(jīng)被使用過,那么我們甚至都可以不發(fā)送這個請求,只需要拋一個異常即可,并且拋異常的信息就是我們?nèi)∠脑?#xff0c;所以我們需要給?CancelToken?擴(kuò)展一個方法。
先修改定義部分。
types/index.ts:
export interface CancelToken {promise: Promise<Cancel> reason?: Cancel throwIfRequested(): void }添加一個?throwIfRequested?方法,接下來實(shí)現(xiàn)它:
cancel/CancelToken.ts:
export default class CancelToken { // ... throwIfRequested(): void { if (this.reason) { throw this.reason } } }判斷如果存在?this.reason,說明這個?token?已經(jīng)被使用過了,直接拋錯。
接下來在發(fā)送請求前增加一段邏輯。
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() } }發(fā)送請求前檢查一下配置的 cancelToken 是否已經(jīng)使用過了,如果已經(jīng)被用過則不用法請求,直接拋異常。
#demo 編寫
在?examples?目錄下創(chuàng)建?cancel?目錄,在?cancel?目錄下創(chuàng)建?index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Cancel example</title> </head> <body> <script src="/__build__/cancel.js"></script> </body> </html>接著創(chuàng)建?app.ts?作為入口文件:
import axios, { Canceler } from '../../src/index' 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)我們的 demo 展示了 2 種使用方式,也演示了如果一個 token 已經(jīng)被使用過,則再次攜帶該 token 的請求并不會發(fā)送。
至此,我們完成了?ts-axios?的請求取消功能,我們巧妙地利用了 Promise 實(shí)現(xiàn)了異步分離。目前官方?axios?庫的一些大的 feature 我們都已經(jīng)實(shí)現(xiàn)了,下面的章節(jié)我們就開始補(bǔ)充完善?ts-axios?的其它功能。
轉(zhuǎn)載于:https://www.cnblogs.com/QianDingwei/p/11403916.html
總結(jié)
以上是生活随笔為你收集整理的axios取消功能的设计与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: react和vue配置本地代理
- 下一篇: axios拦截器的实现