從 axios 源碼中了解到的 Promise 鏈與請求的取消
- 2019 年 10 月 10 日
- 筆記
axios 中一個請求取消的示例: axios 取消請求的示例程式碼import React, { useState, useEffect } from "react"; import axios, { AxiosResponse } from "axios"; export default function App() { const [index, setIndex] = useState(0); const [imgUrl, setImgUrl] = useState(""); useEffect(() => { console.log(`loading ${index}`); const source = axios.CancelToken.source(); axios .get("https://dog.ceo/api/breeds/image/random", { cancelToken: source.token }) .then((res: AxiosResponse<{ message: string; status: string }>) => { console.log(`${index} done`); setImgUrl(res.data.message); }) .catch(err => { if (axios.isCancel(source)) { console.log(err.message); } }); return () => { console.log(`canceling ${index}`); source.cancel(`canceling ${index}`); }; }, [index]); return ( <div> <button onClick={() => { setIndex(index + 1); }} > click </button> <div> <img src={imgUrl} alt="" /> </div> </div> ); }
axios 中一個請求取消的示例 通過解讀其源碼不難實現出一個自己的版本。Here we go… Promise 鏈與攔截器這個和請求的取消其實關係不大,但不妨先來了解一下,axios 中如何組織起來一個 Promise 鏈(Promise chain),從而實現在請求前後可執行一個攔截器(Interceptor)的。 簡單來說,通過 axios 發起的請求,可在請求前後執行一些函數,來實現特定功能,比如請求前添加一些自定義的 header,請求後進行一些數據上的統一轉換等。 用法首先,通過 axios 實例配置需要執行的攔截器: axios.interceptors.request.use(function (config) { console.log('before request') return config; }, function (error) { return Promise.reject(error); }); axios.interceptors.response.use(function (response) { console.log('after response'); return response; }, function (error) { return Promise.reject(error); });
然後每次請求前後都會列印出相應資訊,攔截器生效了。 axios({ url: "https://dog.ceo/api/breeds/image/random", method: "GET" }).then(res => { console.log("load success"); });
下面編寫一個頁面,放置一個按鈕,點擊後發起請求,後續示例中將一直使用該頁面來測試。 import React from "react"; import axios from "axios"; export default function App() { const sendRequest = () => { axios.interceptors.request.use( config => { console.log("before request"); return config; }, function(error) { return Promise.reject(error); } ); axios.interceptors.response.use( response => { console.log("after response"); return response; }, function(error) { return Promise.reject(error); } ); axios({ url: "https://dog.ceo/api/breeds/image/random", method: "GET" }).then(res => { console.log("load success"); }); }; return ( <div> <button onClick={sendRequest}>click me</button> </div> ); }
點擊按鈕後運行結果:
攔截器機制的實現實現分兩步走,先看請求前的攔截器。 請求前攔截器的實現Promise 的常規用法如下: new Promise(resolve,reject);
假如我們封裝一個類似 axios 的請求庫,可以這麼寫: interface Config { url: string; method: "GET" | "POST"; } function request(config: Config) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method, config.url); xhr.onload = () => { resolve(xhr.responseText); }; xhr.onerror = err => { reject(err); }; xhr.send(); }); }
除了像上面那個直接 Promise.resolve(value).then(()=>{ /**... */ });
這種方式創建 Promise 的好處是,我們可以從 function request(config: Config) { return Promise.resolve(config) .then(config => { console.log("interceptor 1"); return config; }) .then(config => { console.log("interceptor 2"); return config; }) .then(config => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method, config.url); xhr.onload = () => { resolve(xhr.responseText); }; xhr.onerror = err => { reject(err); }; xhr.send(); }); }); }
將前面示例中 axios 替換為我們自己寫的
這裡,已經實現了 axios 中請求前攔截器的功能。仔細觀察,上面三個 於是我們可以將他們抽取成三個函數,每個函數就是一個攔截器。 function interceptor1(config: Config) { console.log("interceptor 1"); return config; } function interceptor2(config: Config) { console.log("interceptor 2"); return config; } function xmlHttpRequest<T>(config: Config) { return new Promise<T>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method, config.url); xhr.onload = () => { resolve(xhr.responseText as any); }; xhr.onerror = err => { reject(err); }; xhr.send(); }); }
接下來要做的,就是從 Promise 鏈的頭部 function request<T = any>(config: Config) { let chain: Promise<any> = Promise.resolve(config); chain = chain.then(interceptor1); chain = chain.then(interceptor2); chain = chain.then(xmlHttpRequest); return chain as Promise<T>; }
然後,將上面硬編碼的寫法程式化一下,就實現了任意個請求前攔截器的功能。 擴展配置,以接收攔截器: interface Config { url: string; method: "GET" | "POST"; interceptors?: Interceptor<Config>[]; }
創建一個數組,將執行請求的函數做為默認的元素放進去,然後將用戶配置的攔截器壓入數組前面,這樣形成了一個攔截器的數組。最後再遍歷這個數組形成 Promise 鏈。 function request<T = any>({ interceptors = [], ...config }: Config) { // 發送請求的攔截器為默認,用戶配置的攔截器壓入數組前面 const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest]; interceptors.forEach(interceptor => { tmpInterceptors.unshift(interceptor); }); let chain: Promise<any> = Promise.resolve(config); tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor))); return chain as Promise<T>; }
使用: request({ url: "https://dog.ceo/api/breeds/image/random", method: "GET", interceptors: [interceptor1, interceptor2] }).then(res => { console.log("load success"); });
執行結果:
注意這裡順序為傳入的攔截器的反序,不過這不重要,可通過傳遞的順序來控制。 響應後攔截器上面實現了在請求前執行一序列攔截函數,同理,如果將攔截器壓入到數組後面,即執行請求那個函數的後面,便實現了響應後的攔截器。 繼續擴展配置,將請求與響應的攔截器分開: interface Config { url: string; method: "GET" | "POST"; interceptors?: { request: Interceptor<Config>[]; response: Interceptor<any>[]; }; }
更新 function request<T = any>({ interceptors = { request: [], response: [] }, ...config }: Config) { const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest]; interceptors.request.forEach(interceptor => { tmpInterceptors.unshift(interceptor); }); interceptors.response.forEach(interceptor => { tmpInterceptors.push(interceptor); }); let chain: Promise<any> = Promise.resolve(config); tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor))); return chain as Promise<T>; }
類似 function interceptor3<T>(res: T) { console.log("interceptor 3"); return res; } function interceptor4<T>(res: T) { console.log("interceptor 4"); return res; }
測試程式碼: request({ url: "https://dog.ceo/api/breeds/image/random", method: "GET", interceptors: { request: [interceptor1, interceptor2], response: [interceptor3, interceptor4] } }).then(res => { console.log("load success"); });
運行結果:
不難看出,當我們發起一次 axios 請求時,其實是發起了一次 Promise 鏈,鏈上的函數順次執行。
因為拉弓沒有回頭箭,請求發出後,能夠取消的是後續操作,而不是請求本身,所以上面的 Promise 鏈中,需要實現
請求的取消Promise 鏈的中斷中斷 Promise 鏈的執行,可通過 throw 異常來實現。 添加一個中間函數,將執行請求的函數進行封裝,無論其成功與否,都拋出異常將後續執行中斷。 function adapter(config: Config) { return xmlHttpRequest(config).then( res => { throw "baddie!"; }, err => { throw "baddie!"; } ); }
更新 function request<T = any>({ interceptors = { request: [], response: [] }, ...config }: Config) { - const tmpInterceptors: Interceptor<any>[] = [xmlHttpRequest]; + const tmpInterceptors: Interceptor<any>[] = [adapter]; interceptors.request.forEach(interceptor => { tmpInterceptors.unshift(interceptor); }); interceptors.response.forEach(interceptor => { tmpInterceptors.push(interceptor); }); let chain: Promise<any> = Promise.resolve(config); tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor))); return chain as Promise<T>; }
再次執行其輸出結果為:
請求取消的實現按照 axios 的實現思路,要實現請求的取消,需要先創建一個 token,通過該 token 可調用一個 構造 token所以不難看出,這裡的 token 對象至少:
額外地,
由此我們得到這麼一個類: class CancelTokenSource { private _canceled = false; get canceled() { return this._canceled; } private _message = "unknown reason"; get message() { return this._message; } cancel(reason?: string) { if (this.canceled) return; if (reason) { this._message = reason; } this._canceled = true; } }
添加 token 到配置擴展配置,以接收一個用來取消的 token 對象: interface Config { url: string; method: "GET" | "POST"; + cancelToken?: CancelTokenSource; interceptors?: { request: Interceptor<Config>[]; response: Interceptor<any>[]; }; }
請求邏輯中處理取消同時更新 function xmlHttpRequest<T>(config: Config) { return new Promise<T>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method, config.url); xhr.onload = () => { resolve(xhr.responseText as any); }; xhr.onerror = err => { reject(err); }; + xhr.onabort = () => { + reject(); + }; + if (config.cancelToken) { + xhr.abort(); + } xhr.send(); }); }
取消的調用將拋異常的程式碼抽取成方法以在多處調用,更新 function throwIfCancelRequested(config: Config) { if (config.cancelToken && config.cancelToken.canceled) { throw config.cancelToken.message; } } function adapter(config: Config) { throwIfCancelRequested(config); return xmlHttpRequest(config).then( res => { throwIfCancelRequested(config); return res; }, err => { throwIfCancelRequested(config); return Promise.reject(err); } ); }
測試請求的取消似乎一切 okay,接下來測試一波。以下程式碼期望每次點擊按鈕發起請求,請求前先取消掉之前的請求。為了區分每次不同的請求,添加 import React, { useEffect, useState } from "react"; export default function App() { const [index, setIndex] = useState(0); useEffect(() => { const token = new CancelTokenSource(); request({ url: "https://dog.ceo/api/breeds/image/random", method: "GET", cancelToken: token, interceptors: { request: [interceptor1, interceptor2], response: [interceptor3, interceptor4] } }) .then(res => { console.log(`load ${index} success`); }) .catch(err => { console.log("outer catch ", err); }); return () => { token.cancel(`just cancel ${index}`); }; }, [index]); return ( <div> <button onClick={() => { setIndex(index + 1); }} > click me </button> </div> ); }
載入頁面進行測試, interceptor 2 interceptor 1 interceptor 3 interceptor 4 load 0 success interceptor 2 interceptor 1 interceptor 2 interceptor 1 outer catch just cancel 1 interceptor 3 interceptor 4 load 2 success
現有實現中的問題從輸出來看,
從輸出和網路請求來看,有兩個問題:
function throwIfCancelRequested(config: Config, flag?: number) { if (config.cancelToken && config.cancelToken.canceled) { console.log(flag); throw config.cancelToken.message; } } function adapter(config: Config) { throwIfCancelRequested(config, 1); return xmlHttpRequest(config).then( res => { //ℹ 後續輸出證明,實際生效的是此處 throwIfCancelRequested(config, 2); return res; }, err => { //ℹ 而非此處,即使取消的動作是在請求進行過程中 throwIfCancelRequested(config, 3); return Promise.reject(err); } ); }
輸出: interceptor 2 interceptor 1 interceptor 2 interceptor 1 2 outer catch just cancel 1 interceptor 3 interceptor 4 load 2 success
優化下面的優化需要解決上面的問題。所用到的方法便是 axios 中的邏輯,也是一開始看源碼會不太理解的地方。 其實外部調用 因此,在 更新後的 class CancelTokenSource { public promise: Promise<unknown>; private resolvePromise!: (value?: any) => void; constructor() { this.promise = new Promise(resolve => { this.resolvePromise = resolve; }); } private _canceled = false; get canceled() { return this._canceled; } private _message = "unknown reason"; get message() { return this._message; } cancel(reason?: string) { if (reason) { this._message = reason; } this._canceled = true; this.resolvePromise(); } }
更新後訪問 function xmlHttpRequest<T>(config: Config) { return new Promise<T>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method, config.url); xhr.onload = () => { resolve(xhr.responseText as any); }; xhr.onerror = err => { reject(err); }; xhr.onabort = () => { reject(); }; if (config.cancelToken) { config.cancelToken.promise.then(() => { xhr.abort(); }); } xhr.send(); }); }
測試優化後的版本輸出結果:
瀏覽器調試工具的網路會有一次飄紅被 完整程式碼自己實現的請求取消機制完整程式碼import React, { useState, useEffect } from "react"; class CancelTokenSource { public promise: Promise<unknown>; private resolvePromise!: (value?: any) => void; constructor() { this.promise = new Promise(resolve => { this.resolvePromise = resolve; }); } private _canceled = false; get canceled() { return this._canceled; } private _message = "unknown reason"; get message() { return this._message; } cancel(reason?: string) { if (reason) { this._message = reason; } this._canceled = true; this.resolvePromise(); } } type Interceptor<T> = (value: T) => T | Promise<T>; interface Config { url: string; method: "GET" | "POST"; cancelToken?: CancelTokenSource; interceptors?: { request: Interceptor<Config>[]; response: Interceptor<any>[]; }; } function interceptor1(config: Config) { console.log("interceptor 1"); return config; } function interceptor2(config: Config) { console.log("interceptor 2"); return config; } function interceptor3<T>(res: T) { console.log("interceptor 3"); return res; } function interceptor4<T>(res: T) { console.log("interceptor 4"); return res; } function xmlHttpRequest<T>(config: Config) { return new Promise<T>((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(config.method, config.url); xhr.onload = () => { resolve(xhr.responseText as any); }; xhr.onerror = err => { reject(err); }; xhr.onabort = () => { reject(); }; if (config.cancelToken) { config.cancelToken.promise.then(() => { xhr.abort(); }); } xhr.send(); }); } function throwIfCancelRequested(config: Config, flag?: number) { if (config.cancelToken && config.cancelToken.canceled) { console.log(flag); throw config.cancelToken.message; } } function adapter(config: Config) { throwIfCancelRequested(config, 1); return xmlHttpRequest(config).then( res => { throwIfCancelRequested(config, 2); return res; }, err => { throwIfCancelRequested(config, 3); return Promise.reject(err); } ); } function request<T = any>({ interceptors = { request: [], response: [] }, ...config }: Config) { const tmpInterceptors: Interceptor<any>[] = [adapter]; interceptors.request.forEach(interceptor => { tmpInterceptors.unshift(interceptor); }); interceptors.response.forEach(interceptor => { tmpInterceptors.push(interceptor); }); let chain: Promise<any> = Promise.resolve(config); tmpInterceptors.forEach(interceptor => (chain = chain.then(interceptor))); return chain as Promise<T>; } export default function App() { const [index, setIndex] = useState(0); useEffect(() => { const token = new CancelTokenSource(); request({ url: "https://dog.ceo/api/breeds/image/random", method: "GET", cancelToken: token, interceptors: { request: [interceptor1, interceptor2], response: [interceptor3, interceptor4] } }) .then(res => { console.log(`load ${index} success`); }) .catch(err => { console.log("outer catch ", err); }); return () => { token.cancel(`just cancel ${index}`); }; }, [index]); return ( <div> <button onClick={() => { setIndex(index + 1); }} > click me </button> </div> ); }
運行效果 相關資源 |