阅读了axios源码后对axios的一些最佳实践和axios-retry插件的使用感悟
前言
乘着年底没什么事,把axios和axios-retry的源码简单看了下,总结了下一些最佳实践。
axios本身
axios有个很便捷的使用方式,就是它既可以作为函数发起请求:axios()
,也能通过它身上的属性,如:get、post、delete
等来发起对应请求,也可以通过axios.request()
来发起更加灵活配置请求,甚至还能通过axios.create()
来创建自定义配置的请求实例。
为什么它能有这么多请求方式?其实就是axios本身就是axios.request
这个方法的bind返回值,当我们import引入这个axios的时候,引入的是request
函数。
import axios from "axios";
这个bind的函数会通过“继承”的方式,继承了Axios
类的实例属性和原型属性,实例属性有:defaults、interceptors
,原型属性就是各种请求方法:request、get、post、put、delete、patch
等。
官方还给axios函数挂载了Axios
类(构造函数)和create
创建实例的方法。
由于还有取消,官方也给axios挂载了判断是否是取消请求的判断函数isCancel
。
当然还有很多,这里简单示例下官方入口源码:
function createInstance(defaultConfig) {
const context = new Axios(defaultConfig);
const instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
utils.extend(instance, context, null, {allOwnKeys: true});
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
const axios = createInstance(defaults);
axios.Axios = Axios;
axios.CanceledError = CanceledError;
axios.CancelToken = CancelToken;
axios.isCancel = isCancel;
axios.VERSION = VERSION;
axios.toFormData = toFormData;
axios.AxiosError = AxiosError;
axios.Cancel = axios.CanceledError;
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = spread;
axios.isAxiosError = isAxiosError;
axios.mergeConfig = mergeConfig;
axios.AxiosHeaders = AxiosHeaders;
axios.formToJSON = thing => formDataToJSON(utils.isHTMLForm(thing) ? new FormData(thing) : thing);
axios.getAdapter = adapters.getAdapter;
axios.HttpStatusCode = HttpStatusCode;
axios.default = axios;
export default axios
我们再透过更深的代码,可以发现axios官方提供的get、post
这些方法本身就是基于request
方法做的二次封装。
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(mergeConfig(config || {}, {
method,
headers: isForm ? {
'Content-Type': 'multipart/form-data'
} : {},
url,
data
}));
};
}
所以在前端项目中API这么封装调用,其实也是最佳实践了:
import { request } from "@/request"; // axios项目封装
import type { LoginData, LoginResult } from "./types";
export type * from "./types";
/** 接口示例 */
export function login(data: LoginData) {
return request<LoginResult>({
url: "/auth/login",
method: "post",
data
});
}
通过传入config的形式定义请求才是正统。
axios拦截器和它的执行顺序
axios拦截器想必大家已经很熟悉了,但是它的执行顺序会让人非常迷惑,这个问题到现在也不会修复,不知道官方是怎么想的,但是个人感觉不符合代码书写的逻辑和阅读。
axios实例上挂载着interceptors
拦截器属性,我们可以通过它注册请求拦截器和响应拦截器,从而对数据或者配置做一些处理。
import axios from "axios";
const instance = axios.create({
baseURL: "http://localhost:3000",
});
instance.interceptors.request.use(
(config) => {
return config;
},
(error) => {
console.log("第一个响应错误拦截器", error);
return Promise.reject(error);
}
);
instance.interceptors.response.use(
(response) => {
// 解包
return response.data;
},
(error) => {
console.log("响应错误拦截器", error);
return Promise.reject(error);
}
);
instance
.request({
url: "/",
method: "GET",
})
.then((res) => {
console.log(res);
})
.catch((error) => {
console.log("catch", error);
});
这是一个非常简单的代码示例,我们注册了请求拦截器和响应拦截器,但是会有一个问题:request拦截器的错误响应不会触发,为什么?我们就得看一下官方对拦截器的操作了。
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
chain是一个数组,里面默认传入了两个值,dispatchRequest
是用于发起实际的请求方法,undefined
则是占位。
unshift
是将请求拦截器插入到了chain前面,我们请求拦截器注册了两个方法,假设为:fn1、fn2
,那么chain如下表示:
[fn1, fn2, dispatchRequest.bind(this), undefined];
push
则是将响应拦截器插入到数组末尾,假设方法为:fn3、fn4
,那么chain如下表示:
[fn1, fn2, dispatchRequest.bind(this), undefined, fn3, fn4];
接着会创建一个成功状态的promise,传入了config作为resolve的值,然后while循环,每次从数组取两个方法,作为这个promise.then
的参,这两个参就是成功和失败回调。
最终会将数组的值全部取完,生成一条Promise链条。
到这里你应该就明白了,axios通过非常巧妙的方式实现了顺序执行,但是成也萧何败萧何,正因为一开始就是Promise.resolve(config)
,导致请求拦截器中的错误处理,“永远”都不会触发。
所以,如果你的项目中只有一个请求拦截器,那么这个错误处理,可以不写。
但是真的不会触发吗?
其实也不然,从代码的角度看,只是首个不会被触发而已,当我们注册了两个request的异常处理,其实还是能触发一个的。
instance.interceptors.request.use(
(config) => {
throw new Error("请求错误拦截器");
},
(error) => {
console.log("第一个响应错误拦截器", error);
return Promise.reject(error);
}
);
instance.interceptors.request.use(
(config) => {
return config;
},
(error) => {
console.log("第二个响应错误拦截器", error);
return Promise.reject(error);
}
);
于是理所当然的,我们注册两个request拦截器,并在第一个拦截器里面,通过throw抛出错误,然后在第二个拦截器中log打印,理论上控制台应该输出:
第二个响应错误拦截器 xxx
但是事实就是不会触发。为什么?我们看下requestInterceptorChain
这个数组。
const requestInterceptorChain = [];
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
去除一些不重要的内容,核心代码如下:
const requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
可以看到request被forEach遍历,然后从遍历的interceptor
中取出了fulfilled
和rejected
回调然后unshift插入到数组,注意,问题就在这了。
假设我们刚刚两个request拦截器函数回调为:fn1、fn2、fn3、fn4
,那么两个interceptor对象如下:
// 第一个
{
fulfilled: fn1,
rejected: fn2
}
// 第二个
{
fulfilled: fn3,
rejected: fn4
}
在unshift
插入的时候,它是在数组开头插入,这就导致遍历的越往后,最终会排在最前面,最终requestInterceptorChain的值为:
[fn3, fn4, fn1, fn2]
这个数组又会被整个插入到chain
数组中:
[fn3, fn4, fn1, fn2, dispatchRequest.bind(this), undefined];
到这里你应该就明白了,这个axios的request拦截器,它最终居然是倒序运行的。
所以如果我们想要触发request异常拦截器,就要反着写:
instance.interceptors.request.use(
(config) => {
return config;
},
(error) => {
console.log("第一个响应错误拦截器", error);
return Promise.reject(error);
}
);
instance.interceptors.request.use(
(config) => {
throw new Error("请求错误拦截器");
},
(error) => {
console.log("第二个响应错误拦截器", error);
return Promise.reject(error);
}
);
我们在第二个拦截器中throw,就能在第一个拦截器中捕获到了。
最终就会打印:
第一个响应错误拦截器 error
如果是response拦截器,它则不会有这个问题,因为它是push的方式,所以它的顺序就是拦截器use的顺序。
解决拦截器顺序问题的一些方式
一般会有两种推荐方式:
- 第一种就是只写一个request拦截器,然后在这个拦截器里面把所有处理都做了,这样就不会因为多个拦截器导致的顺序问题。
- 第二种就是在所有request拦截器添加完成后,执行这个代码:
axios.request.interceptors.handlers.reverse();
,我们手动将request拦截器中的handlers这个回调函数数组reverse反向一下,这样后面unshift插入的时候,不就是正序了。
axios-retry失败重试
这个插件只用了两个拦截回调,一个是请求前给request的config中添加axios-retry
的配置对象,二就是在响应拦截器中的异常回调,在这里做失败重试处理。
const axiosRetry: AxiosRetry = (axiosInstance, defaultOptions) => {
const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
setCurrentState(config, defaultOptions, true);
if (config[namespace]?.validateResponse) {
// by setting this, all HTTP responses will be go through the error interceptor first
config.validateStatus = () => false;
}
return config;
});
const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
const { config } = error;
// If we have no information to retry the request
if (!config) {
return Promise.reject(error);
}
const currentState = setCurrentState(config, defaultOptions);
if (error.response && currentState.validateResponse?.(error.response)) {
// no issue with response
return error.response;
}
if (await shouldRetry(currentState, error)) {
return handleRetry(axiosInstance, currentState, error, config);
}
await handleMaxRetryTimesExceeded(currentState, error);
return Promise.reject(error);
});
return { requestInterceptorId, responseInterceptorId };
};
重点是失败重试的判断条件:
- 如果不存在config对象不进行重试;
- 如果存在response属性,且用户配置了
validateResponse
自定义校验响应值的方式,且校验通过也不会重试; - 通过
shouldRetry
方法判断是否需要重试,这个方法会先判断是否到了最大重试次数,没有则调用retryCondition
方法判断是否可以重试,这个方法用户可以自定义,如果没有则使用isNetworkOrIdempotentRequestError
这个默认函数判断是否是网络或者幂等请求错误。
知道了条件,我们在后续使用的时候就会很方便了,比如我就会这样写:
// 失败重试
axiosRetry(instance, {
retries: 3,
retryCondition(error) {
const config = error?.config;
if (!config) return false;
if (config.enableRetry && isNetworkOrIdempotentRequestError(error)) {
return true;
}
return false;
},
onMaxRetryTimesExceeded: (error) => {
// 显示错误消息
showMaxRetryErrorMessage(error);
}
});
通过自定义retryCondition
来判断是否需要重试,我会判断config上有没有配置自定义的enableRetry
属性,并且还会调用isNetworkOrIdempotentRequestError
方法来实现默认判定效果
并且我们需要注意拦截器的执行顺序,所以失败重试一般要写在其他拦截器的最前面,这样才能保证response拦截器第一个触发的就是失败重试。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
全部评论 1
wu先生
Google Chrome Windows 10