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> ); }
点击按钮后运行结果:
before request after response load success
拦截器机制的实现
实现分两步走,先看请求前的拦截器。
请求前拦截器的实现
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(); }); }
除了像上面那个直接
new一个 Promise 外,其实任意对象值都可以形成一个 Promise,方法是调用
Promise.resolve,
Promise.resolve(value).then(()=>{ /**... */ });
这种方式创建 Promise 的好处是,我们可以从
config开始,创建一个 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 替换为我们自己写的
request函数,示例可以正常跑起来,输出如下:
interceptor 1 interceptor 2 load success
这里,已经实现了 axios 中请求前拦截器的功能。仔细观察,上面三个
then当中的函数,形成了一个 Promise 链,在这个链中顺次执行,每一个都可以看成一个拦截器,即使是执行发送请求的那个
then。
于是我们可以将他们抽取成三个函数,每个函数就是一个拦截器。
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 链的头部
Promise.resolve(config)开始,将上面三个函数串起来。借助 Monkey patch 这不难实现:
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: 1c2c "GET", interceptors: [interceptor1, interceptor2] }).then(res => { console.log("load success"); });
执行结果:
interceptor 2 interceptor 1 load success
注意这里顺序为传入的拦截器的反序,不过这不重要,可通过传递的顺序来控制。
响应后拦截器
上面实现了在请求前执行一序列拦截函数,同理,如果将拦截器压入到数组后面,即执行请求那个函数的后面,便实现了响应后的拦截器。
继续扩展配置,将请求与响应的拦截器分开:
interface Config { url: string; method: "GET" | "POST"; interceptors?: { request: Interceptor<Config>[]; response: Interceptor<any>[]; }; }
更新
request方法,请求前拦截器的逻辑不变,将新增的响应拦截器通过
push压入数组后面:
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>; }
类似
interceptor1
interceptor2,新增两个拦截器用于响应后执行,
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"); });
运行结果:
interceptor 2 interceptor 1 interceptor 3 interceptor 4 load success
不难看出,当我们发起一次 axios 请求时,其实是发起了一次 Promise 链,链上的函数顺次执行。
request interceptor 1 request interceptor 2 ... request response interceptor 1 response interceptor 2 ...
因为拉弓没有回头箭,请求发出后,能够取消的是后续操作,而不是请求本身,所以上面的 Promise 链中,需要实现
request之后的拦截器和后续回调的取消执行。
request interceptor 1 request interceptor 2 ... request # 🚫 后续操作不再执行 response interceptor 1 response interceptor 2 ...
请求的取消
Promise 链的中断
中断 Promise 链的执行,可通过 throw 异常来实现。
添加一个中间函数,将执行请求的函数进行封装,无论其成功与否,都抛出异常将后续执行中断。
function adapter(config: Config) { return xmlHttpRequest(config).then( res => { throw "baddie!"; }, err => { throw "baddie!"; } ); }
更新
request函数使用
adapter而不是直接使用
xmlHttpRequest:
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>; }
再次执行其输出结果为:
interceptor 2 interceptor 1 Uncaught (in promise) baddie!
请求取消的实现
按照 axios 的实现思路,要实现请求的取消,需要先创建一个 token,通过该 token 可调用一个
cancel方法;通过将 token 传递到配置中,在发起请求时对 token 进行检查以判定该 token 是否执行过取消,如果是则利用上面的思路,将 Promise 链中断掉。
构造 token
所以不难看出,这里的 token 对象至少:
有一个cancel方法 有一个字段记录
cancel方法是否被调用过
额外地,
如果有一个字段记录取消的原因,那也不错。由此我们得到这么一个类:
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>[]; }; }
请求逻辑中处理取消
同时更新
xmlHttpRequest函数,判断 token 的状态是否调用过取消,如果是则调用
x 3ff8 hr.abort(),同时添加
onabort回调以 reject 掉 Promise:
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(); }); }
取消的调用
将抛异常的代码抽取成方法以在多处调用,更新
adapter的逻辑,在没有取消的情况下正常返回和 reject。
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,接下来测试一波。以下代码期望每次点击按钮发起请求,请求前先取消掉之前的请求。为了区分每次不同的请求,添加
index变量,按钮点击时自增。
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> ); }
加载页面进行测试,
useEffect会在页面加载后首次运行,会触发一次完整的请求流程。然后连续点击两次按钮,以取消掉两次中的前一次。运行结果:
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
现有实现中的问题
从输出来看,
第一部分为首次请求,是一次正常的请求。 第二部分为第一次点击的请求拦截器的执行。 第三部分为第二次点击,将第一次请求进行了取消,然后完成一次完整的请求。从输出和网络请求来看,有两个问题:
xhr.abort()没有生效,连续的两次点击中,浏览器调试工具中会有两条状态为 200 的请求。 第一条请求后续的回调确实被取消掉了,但它是在等待请求成功后,在成功回调中取消的,这点可通过在取消函数中添加标志位来查看。
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 中的逻辑,也是一开始看源码会不太理解的地方。
其实外部调用
cancel()的时机并不确定,所以 token 对象上记录其是否被取消的字段,何时被置为
true是不确定的,因此,我们取消请求的逻辑(
xhr.abort())应该是在一个 Promise 中来完成。
因此,在
CancelTokenSource类中,创建一个 Promise 类型的字段,它会在
cancel()方法被调用的时候 resolve 掉。
更新后的
CancelTokenSource类:
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(); } }
更新后访问
canceled字段的逻辑:
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(); }); }
测试优化后的版本
输出结果:
interceptor 2 interceptor 1 interceptor 3 interceptor 4 load 0 success interceptor 2 interceptor 1 interceptor 2 3 interceptor 1 outer catch just cancel 1 interceptor 3 interceptor 4 load 2 success
浏览器调试工具的网络会有一次飘红被
abort掉的请求,同时上面的输出(生效的地方是 3 而非 2)显示被取消的请求正确地 reject 掉了。
完整代码
自己实现的请求取消机制完整代码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> ); }
运行效果
相关资源
axiosThe text was updated successfully, but these errors were encountered: