如何不用拦截器为 Axios 扩展功能
背景
在笔者日常工作中,均是以 axios
作为前端应用里请求后端接口数据的工具,它的优势本文不再叙述,这里对如何便于使用 axios
且如何让其功能可复制、可扩展、易移植进行探讨。
拦截器的问题
axios
的拦截器功能想必大家都不陌生,主要用于在请求和响应后对配置项或数据状态做一些额外处理,但本文并不会使用这个功能作为主题,但这里仍然要说明下原因。
- 扩展不同功能之间高度耦合,不易拆分和维护
- 扩展功能不易移植
- 功能越多越不易扩展
功能示例
取消重复请求功能
export const http = axios.create({ baseURL: '/api' })
// 取消重复请求功能的通用做法
// 使用 map 记录接口数据
export const pendingServices = new Map()
/**
* 生成接口标记
*
* @param {import('axios').AxiosRequestConfig} config 请求配置项
*/
function createMark(config = {}) {
const { method = 'get', url = '', data = {}, params = {} } = config
// 请求之后配置项会被转为字符串,为了保证请求与响应一致这里进行额外处理
const processValue = (value) =>
typeof value === 'string' ? safeJsonParse(value, value) : JSON.parse(JSON.stringify(value))
// 序列化参数生成唯一标识
return serialize({
method,
url,
data: processValue(data),
params: processValue(params)
})
}
/**
* 接口取消类型
*/
export const ServiceCancelTypeEnum = {
/**
* 超时取消
*/
timeout: 'timeout',
/**
* 手动取消
*/
manual: 'manual'
}
/**
* 是否手动取消请求
*
* @param {string} cancelType 接口请求取消类型
*
* @returns {boolean}
*/
export function isManualCancel(cancelType) {
return cancelType === ServiceCancelTypeEnum.manual
}
/**
* 添加接口请求
*
* @description 以节流思维避免接口重复请求。
*
* @param {import('axios').AxiosRequestConfig} config 接口配置项
*
* @example
* ```js
* http.get('/api') // A
* http.get('/api') // B
*
* // B 被取消
* // pendingServices 只存在 A 的信息
* ```
*/
function addPendingService(config = {}) {
// 如果允许请求重复或已经携带了 cancelToken 则直接返回
if (!config.$cancelRepeatRequest || !!config.cancelToken) return
// 生成接口标识
const mark = createMark(config)
// 判断是否已经存在当前标识
if (pendingServices.has(mark)) {
// 请求重复时取消本次调用
config.cancelToken = new Axios.CancelToken((cancel) => {
// 记录当前接口取消类型
config.$cancelType = ServiceCancelTypeEnum.manual
// 记录的当前接口是由于重复请求而取消
config.$isRepeatCancel = true
// cancel 函数传入的数据将被作为响应失败后的 error.message
cancel(config)
})
} else {
// 由于 CancelToken 回调为异步,所以这里先留存记录
pendingServices.set(mark, { mark, cancel: null, config })
config.cancelToken = new Axios.CancelToken((cancel) => {
pendingServices.get(mark).cancel = cancel
})
}
}
/**
* 移除 pendingServices 中的接口请求
*
* @param {import('axios').AxiosRequestConfig} config 接口配置项
*/
function removePendingService(config = {}) {
// 如果是重复请求则直接返回
if (config.$isRepeatCancel) return
const mark = createMark(config)
if (!pendingServices.has(mark)) return null
const service = pendingServices.get(mark)
pendingServices.delete(mark)
return service
}
/**
* 错误响应时获取对应的请求配置项
*
* @param {any} error 错误数据
*
* @returns {import('axios').AxiosRequestConfig}
*/
function getErrorConfig(error) {
if (error.config) return error.config
return typeof error.message === 'object' ? error.message : null
}
// 请求拦截
http.interceptors.request.use((config) => {
// 取消重复请求,默认 true
config.$cancelRepeatRequest ??= true
// 取消类型,用于区分重复取消和超时取消
config.$cancelType = undefined
// 是否为重复取消的请求
config.$isRepeatCancel = false
// 添加接口记录
addPendingService(config)
return config
})
// 响应拦截
http.interceptors.response.use(
(response) => {
// 移除接口记录
removePendingService(response.config)
return response.data
},
(error) => {
// 获取错误信息里的请求配置项
const config = getErrorConfig(error)
// 判断是否为取消请求,接口超时时 isCancel 为 false ,所以需要判断 message
const isCancel = Axios.isCancel(error) || error.message?.indexOf('timeout') > -1
// 获取取消类型
const cancelType = isCancel ? getCancelType(config) : ''
// 根据配置项移除请求记录
removePendingService(config)
if (isManualCancel(cancelType)) {
// 这里只简单的为重复取消兜底,若存在响应包装可自行处理
return Promise.resolve({ data: { isManualCancel: true } })
}
// 往上抛出错误
return Promise.reject(new Error(error))
}
)
vue
中使用示例:
export default {
methods: {
async loadData() {
try {
// 默认取消重复请求
const response = http.get('/demo/list')
// 如果是重复取消或组件已被卸载则终止后续操作
if (response.data.isManualCancel || this._isDestroyed) return
// do somethings...
} catch(err) {
console.log(err.message)
}
}
}
}
以上是对接口重复请求的封装,属于较为常见功能,但是我们仍然需要完成整个请求流程,从请求到响应结束均会执行拦截器代码,无法在判断重复时直接终止请求操作。
这样的情况会导致我们在拦截器扩展的其他功能时,需要在重复请求取消时添加一些额外逻辑以保证流程可以正常执行,但如此反而增加了不同功能之间的耦合度。
问题探讨
上述的问题也许有朋友就要说:“我可以把不同功能的代码拆分到不同文件里,仍然很好维护啊。”
这一方案笔者也尝试过,但是不同的功能所执行的时机是不同的,而且由于项目中为了避免 try...catch
逻辑,通常会将接口响应二次包装,通过不同的数据结构以体现成功和失败,让代码看起来更顺畅且更健壮(遗漏了错误兜底),这样的好处显而易见,但是对我们需要扩展其他功能来说需要考虑的点也会更多,所以这个方案仅仅能优化 http
文件里的代码不至于太过冗余。
其次就是不同的项目可能需要不同的功能,但就算耦合度较低的代码迁移起来仍然会涉及到源码的更改,所以拦截器方案仍然无法普及适用。
解决思路
笔者曾反复思考后,认为问题既是由于拦截器引起,那么还是得从拦截器方面入手,axios
自带的拦截器既然无法满足需要,那么是否可以通过模拟拦截器功能从而实现我们的需求呢?
方案落地
经过探索后,结合 vue
的 hooks
使用经历,开发了一个插件 axios-ext
,通过注册不同插件进而为 axios
实现不同功能。
使用示例
import { createAxios } from '@iel/axios-ext'
import AxiosExtCancelRepeat from '@iel/axios-ext-cancel-repeat'
import axios from 'axios'
// 接收 axios 配置项或实例并返回包装后的 axios 实例
// http.$axiosExt 为 AxiosExt 实例
const http = createAxios(axios)
// 注册该插件,默认会执行插件方法体内部函数
// 返回该实例,已注册插件不会被重复注册
http.$axiosExt
// 注册取消重复请求插件
.use(AxiosExtCancelRepeat, {
manualCancelMessage: '手动取消接口',
onRepeat: () => [true, '取消重复接口']
})
// 注册响应包装插件
.use(AxiosExtResponseWrap, {
wrapper: AxiosResponseTupleWrapper([ErrorAdaptor, SuccessAdaptor])
})
// 销毁实例,在插件销毁时处理一些事情并清理所有插件信息
// http.$axiosExt.destroy()
示例截图
const loadData = async () => {
const response = await http.get('/demo/list')
console.log(response) // response 为下图中的 returnValue
}
测试页面
可在该页面进行测试!
系列插件
axios-ext
扩展插件axios-ext-cache
扩展缓存功能axios-ext-cancel-repeat
扩展取消重复请求功能axios-ext-log
扩展插件日志axios-ext-response-wrap
扩展响应包装功能axios-ext-retry
扩展失败重连功能
预设插件
解决繁琐的配置项和包安装步骤。
axios-ext-preset
插件功能预设
结尾
以上是笔者目前对 axios
扩展功能的想法及解决方案,也许受限于能力可能略有不足还请各位看官谅解,各位道友若有问题可在评论区留言!
转载自:https://juejin.cn/post/7091625049953665061