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?的其它功能。
轉載于:https://www.cnblogs.com/QianDingwei/p/11403916.html
總結
以上是生活随笔為你收集整理的axios取消功能的设计与实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: react和vue配置本地代理
- 下一篇: axios拦截器的实现