本文将使用TS开发一个Axios插件,用来解决在某些情况下接口请求错误时,需要自动重试的问题。
功能
- 接口错误时,能够自动重试并返回最终结果;
- 可自定义最大重试次数、重试间隔时间;
- 可自定义何时重试,即自定义何为接口错误;
- 提供开始重试之前执行的钩子函数,可执行一些自定义逻辑;
- 提供重试失败回调方法;
- 无缝集成到axios中。
开发
核心实现
以下代码实现了一个最基本的请求重试插件,仅在接口的http状态错误时,才自动重试。
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
export interface IOptions {
maxRetryCount: number
retryDelay: number
request: (config: InternalAxiosRequestConfig) => Promise<AxiosResponse>
}
// 将最大重试次数与重试间隔参数改为可选参数
export type IConfig = Partial<IOptions> & Pick<IOptions, 'request'>
export class AxiosRetryPlugin {
private options: IOptions
private histories: Map<string, number>
constructor(options: IConfig) {
this.options = {
maxRetryCount: options.maxRetryCount ?? 3,
retryDelay: options.retryDelay ?? 1000,
request: options.request
}
this.histories = new Map()
}
static getDataType(obj: any) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
static generateRequestKey(config: InternalAxiosRequestConfig): string {
const { method, url, data, params } = config
let key = `${method}-${url}`
try {
switch (AxiosRetryPlugin.getDataType(data)) {
case 'object':
key += `-${JSON.stringify(data)}`
break
case 'formdata':
for (const [k, v] of data.entries()) {
if (v instanceof Blob) {
continue
}
key += `-${k}-${v}`
}
break
default:
break
}
if (AxiosRetryPlugin.getDataType(params) === 'object') {
key += `-${JSON.stringify(params)}`
}
} catch (e) {/* empty */}
return key
}
private fetch(config: InternalAxiosRequestConfig) {
return new Promise<AxiosResponse>((resolve, reject) => {
if (!this.options.retryDelay || this.options.retryDelay < 0) {
this.options.request(config).then(resolve).catch(reject)
return
}
setTimeout(() => {
this.options.request(config).then(resolve).catch(reject)
}, this.options.retryDelay)
})
}
private retryRequest(error: AxiosError) {
let config: InternalAxiosRequestConfig
if (!error.config) {
return Promise.reject(error)
}
config = error.config
const key = AxiosRetryPlugin.generateRequestKey(config)
const retryCount = this.histories.get(key) || 0
if (retryCount >= this.options.maxRetryCount) {
this.histories.delete(key)
return Promise.reject(error)
}
this.histories.set(key, retryCount + 1)
return this.fetch(config)
}
responseInterceptorRejected(error: AxiosError) {
return this.retryRequest(error)
}
}
// 为什么用bind绑定this?因为注册axios拦截器时,axios会改变this指向
export default function createAxiosRetryPlugin(config: IConfig) {
const instance = new AxiosRetryPlugin(config)
return {
responseInterceptorRejected: instance.responseInterceptorRejected.bind(instance)
}
}
使用示例:
import axios from 'axios'
import createAxiosRetryPlugin from '..'
const retryPlugin = createAxiosRetryPlugin({ request: axios.request })
axios.interceptors.response.use(undefined, retryPlugin.responseInterceptorRejected)
仅需创建一个插件实例,然后将实例的响应失败拦截器方法注册到axios的响应拦截器中即可。
高级功能(完整代码)
可能有些时候我们需要,自己决定何时自动重试,例如:根据响应数据的code决定,而不是http状态。
也可能我们需要在重试之前,需要执行一些自定义的逻辑代码。
那我们接下来就来添加这些功能。
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
export interface IOptions<
T extends AxiosError = AxiosError,
U extends AxiosResponse = AxiosResponse
> {
maxRetryCount: number
retryDelay: number
request: (config: InternalAxiosRequestConfig) => Promise<AxiosResponse>
isRetry: (error?: T, res?: U) => boolean
beforeRetry?: (retryCount: number, error?: T, res?: U) => void
failed?: (retryCount: number, error?: T, res?: U) => void
}
// 将最大重试次数与重试间隔参数改为可选参数
export type IConfig = Partial<IOptions> & Pick<IOptions, 'request'>
export class AxiosRetryPlugin {
private options: IOptions
private histories: Map<string, number>
constructor(options: IConfig) {
this.options = {
maxRetryCount: options.maxRetryCount ?? 3,
retryDelay: options.retryDelay ?? 1000,
request: options.request,
isRetry:
options.isRetry ??
((err) => {
if (err) {
return true
}
return false
})
}
this.histories = new Map()
}
static getDataType(obj: any) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
static generateRequestKey(config: InternalAxiosRequestConfig): string {
const { method, url, data, params } = config
let key = `${method}-${url}`
try {
switch (AxiosRetryPlugin.getDataType(data)) {
case 'object':
key += `-${JSON.stringify(data)}`
break
case 'formdata':
for (const [k, v] of data.entries()) {
if (v instanceof Blob) {
continue
}
key += `-${k}-${v}`
}
break
default:
break
}
if (AxiosRetryPlugin.getDataType(params) === 'object') {
key += `-${JSON.stringify(params)}`
}
} catch (e) {/* empty */}
return key
}
private fetch(config: InternalAxiosRequestConfig, beforeRetry?: () => void) {
return new Promise<AxiosResponse>((resolve, reject) => {
if (!this.options.retryDelay || this.options.retryDelay < 0) {
// 新增的重试之前执行的钩子函数
beforeRetry && beforeRetry()
this.options.request(config).then(resolve).catch(reject)
return
}
setTimeout(() => {
// 新增的重试之前执行的钩子函数
beforeRetry && beforeRetry()
this.options.request(config).then(resolve).catch(reject)
}, this.options.retryDelay)
})
}
private retryRequest(error: AxiosError): Promise<AxiosResponse>
private retryRequest(error: undefined, res: AxiosResponse): AxiosResponse
private retryRequest(error?: AxiosError, res?: AxiosResponse) {
// 判断是否重试
if (!this.options.isRetry(error, res)) {
if (error) {
return Promise.reject(error)
}
return res!
}
let config: InternalAxiosRequestConfig
if (error) {
if (!error.config) {
return Promise.reject(error)
}
config = error.config
} else {
config = res!.config
}
const key = AxiosRetryPlugin.generateRequestKey(config)
const retryCount = this.histories.get(key) || 0
if (retryCount >= this.options.maxRetryCount) {
// 新增重试失败事件
this.options.failed?.(retryCount, error, res)
this.histories.delete(key)
if (error) {
return Promise.reject(error)
}
return res!
}
const newCount = retryCount + 1
this.histories.set(key, newCount)
return this.fetch(config, () => {
this.options.beforeRetry?.(newCount, error, res)
})
}
// 新增的响应成功处理方法
responseInterceptorFulfilled(response: AxiosResponse) {
return this.retryRequest(undefined, response)
}
responseInterceptorRejected(error: AxiosError) {
return this.retryRequest(error)
}
}
// 为什么用bind绑定this?因为注册axios拦截器时,axios会改变this指向
export default function createAxiosRetryPlugin(config: IConfig) {
const instance = new AxiosRetryPlugin(config)
return {
responseInterceptorFulfilled: instance.responseInterceptorFulfilled.bind(instance),
responseInterceptorRejected: instance.responseInterceptorRejected.bind(instance)
}
}
使用示例:
import axios from 'axios'
import createAxiosRetryPlugin from '..'
const retryPlugin = createAxiosRetryPlugin({
request: axios.request,
isRetry: (error?: AxiosError, res?: AxiosResponse) => {
// 自定义何时重试
return res?.data?.statusCode !== 200 || error?.response?.status === 500
}
})
axios.interceptors.response.use(retryPlugin.responseInterceptorFulfilled, retryPlugin.responseInterceptorRejected)
至此,我们就完成了一个提供请求重试功能的axios插件开发了。
js版本
请求重试插件js版本代码,用法同上。
export class AxiosRetryPlugin {
options = {}
histories = new Map()
constructor(options) {
this.options = {
request: options.request,
maxRetryCount: options.maxRetryCount ?? 3,
retryDelay: options.retryDelay ?? 1000,
isRetry:
options.isRetry ??
((err) => {
if (err) {
return true
}
return false
})
}
this.histories = new Map()
}
static getDataType(obj) {
let res = Object.prototype.toString.call(obj).split(' ')[1]
res = res.substring(0, res.length - 1).toLowerCase()
return res
}
static generateRequestKey(config) {
const { method, url, data, params } = config
let key = `${method}-${url}`
try {
switch (AxiosRetryPlugin.getDataType(data)) {
case 'object':
key += `-${JSON.stringify(data)}`
break
case 'formdata':
for (const [k, v] of data.entries()) {
if (v instanceof Blob) {
continue
}
key += `-${k}-${v}`
}
break
default:
break
}
if (AxiosRetryPlugin.getDataType(params) === 'object') {
key += `-${JSON.stringify(params)}`
}
} catch (e) {
/* empty */
}
return key
}
fetch(config, beforeRetry) {
return new Promise((resolve, reject) => {
if (!this.options.retryDelay || this.options.retryDelay < 0) {
beforeRetry && beforeRetry()
this.options.request(config).then(resolve).catch(reject)
return
}
setTimeout(() => {
beforeRetry && beforeRetry()
this.options.request(config).then(resolve).catch(reject)
}, this.options.retryDelay)
})
}
retryRequest(error, res) {
if (!this.options.isRetry(error, res)) {
if (error) {
return Promise.reject(error)
}
return res
}
let config
if (error) {
if (!error.config) {
return Promise.reject(error)
}
config = error.config
} else {
config = res.config
}
const key = AxiosRetryPlugin.generateRequestKey(config)
const retryCount = this.histories.get(key) || 0
if (retryCount >= this.options.maxRetryCount) {
this.options.failed?.(retryCount, error, res)
this.histories.delete(key)
if (error) {
return Promise.reject(error)
}
return res
}
const newCount = retryCount + 1
this.histories.set(key, newCount)
return this.fetch(config, () => {
this.options.beforeRetry?.(newCount, error, res)
})
}
responseInterceptorFulfilled(response) {
return this.retryRequest(undefined, response)
}
responseInterceptorRejected(error) {
return this.retryRequest(error)
}
}
/**
* 请求重试插件
* @param {object} config
* @param {function} config.request 请求方法,返回Promise,接收axios的config参数
* @param {number} [config.maxRetryCount] 最大重试次数,默认3
* @param {number} [config.retryDelay] 重试间隔时间,默认1000ms
* @param {function} [config.isRetry] 是否重试的判断函数,接收error和response,返回boolean
* @param {function} [config.beforeRetry] 重试前的回调函数,接收重试次数、error和response
* @param {function} [config.failed] 重试失败的回调函数,接收重试次数、error和response
* @returns
*/
export default function createAxiosRetryPlugin(config) {
const instance = new AxiosRetryPlugin(config)
return {
responseInterceptorFulfilled: instance.responseInterceptorFulfilled.bind(instance),
responseInterceptorRejected: instance.responseInterceptorRejected.bind(instance)
}
}