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 類實現

接下來,我們就來實現這個 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 是實例類型的接口定義,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) }) } } 

在 CancelToken 構造函數內部,實例化一個 pending 狀態的 Promise 對象,然后用一個 resolvePromise 變量指向 resolve 函數。接着執行 executor 函數,傳入一個 cancel 函數,在 cancel 函數內部,會調用 resolvePromise 把 Promise 對象從 pending 狀態變為 resolved 狀態。

接着我們在 xhr.ts 中插入一段取消請求的邏輯。

core/xhr.ts

const { /*....*/ cancelToken } = config if (cancelToken) { cancelToken.promise.then(reason => { request.abort() reject(reason) }) } 

這樣就滿足了第二種使用方式,接着我們要實現第一種使用方式,給 CancelToken 擴展靜態接口。

#CancelToken 擴展靜態接口

#接口定義

types/index.ts

export interface CancelTokenSource { token: CancelToken cancel: Canceler } export interface CancelTokenStatic { new(executor: CancelExecutor): CancelToken source(): CancelTokenSource } 

其中 CancelTokenSource 作為 CancelToken 類靜態方法 source 函數的返回值類型,CancelTokenStatic 則作為 CancelToken 類的類類型。

#代碼實現

cancel/CancelToken.ts

export default class CancelToken { // ... static source(): CancelTokenSource { let cancel!: Canceler const token = new CancelToken(c => { cancel = c }) return { cancel, token } } } 

source 的靜態方法很簡單,定義一個 cancel 變量實例化一個 CancelToken 類型的對象,然后在 executor 函數中,把 cancel 指向參數 c 這個取消函數。

這樣就滿足了我們第一種使用方式,但是在第一種使用方式的例子中,我們在捕獲請求的時候,通過 axios.isCancel 來判斷這個錯誤參數 e 是不是一次取消請求導致的錯誤,接下來我們對取消錯誤的原因做一層包裝,並且把給 axios 擴展靜態方法

#Cancel 類實現及 axios 的擴展

#接口定義

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 是實例類型的接口定義,CancelStatic 是類類型的接口定義,並且我們給 axios 擴展了多個靜態方法。

#代碼實現

我在 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 類型的實例。

先修改定義部分。

types/index.ts

export interface CancelToken { promise: Promise<Cancel> reason?: Cancel } 

再修改實現部分:

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 擴展一些靜態方法,供用戶使用。

axios.ts

import CancelToken from './cancel/CancelToken' import Cancel, { isCancel } from './cancel/Cancel' axios.CancelToken = CancelToken axios.Cancel = Cancel axios.isCancel = isCancel 

#額外邏輯實現

除此之外,我們還需要實現一些額外邏輯,比如當一個請求攜帶的 cancelToken 已經被使用過,那么我們甚至都可以不發送這個請求,只需要拋一個異常即可,並且拋異常的信息就是我們取消的原因,所以我們需要給 CancelToken 擴展一個方法。

先修改定義部分。

types/index.ts

export interface CancelToken { promise: Promise<Cancel> reason?: Cancel throwIfRequested(): void } 

添加一個 throwIfRequested 方法,接下來實現它:

cancel/CancelToken.ts

export default class CancelToken { // ... throwIfRequested(): void { if (this.reason) { throw this.reason } } } 

判斷如果存在 this.reason,說明這個 token 已經被使用過了,直接拋錯。

接下來在發送請求前增加一段邏輯。

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() } } 

發送請求前檢查一下配置的 cancelToken 是否已經使用過了,如果已經被用過則不用法請求,直接拋異常。

#demo 編寫

在 examples 目錄下創建 cancel 目錄,在 cancel 目錄下創建 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> 

接着創建 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 已經被使用過,則再次攜帶該 token 的請求並不會發送。

至此,我們完成了 ts-axios 的請求取消功能,我們巧妙地利用了 Promise 實現了異步分離。目前官方 axios 庫的一些大的 feature 我們都已經實現了,下面的章節我們就開始補充完善 ts-axios 的其它功能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM