起因,在项目中发现多个地方同时请求同一个接口且参数相同时,只有最后一个接口能正常返回,并且只有一个地方会展示查询结果...这个问题让我想起了在封装Axios
时,参考网上的例子直接抄来用。
错误方式
以上这种情况发出了8个相同的请求,前7个被取消了,只有最后一个生效,这种会导致前面的请求都报错,得不到想要的结果,先来分析一下以上的实现原理,下面贴出关键代码。
js
// fetch.js
import axios from 'axios';
const service = axios.create({
baseURL: '/api',
// validateStatus: (status) => status <= 500, // 拦截状态码大于或等于500
headers: {
common: { Accept: 'application/json; charset=UTF-8' },
patch: { 'Content-Type': 'application/json; charset=UTF-8' },
post: { 'Content-Type': 'application/json; charset=UTF-8' },
put: { 'Content-Type': 'application/json; charset=UTF-8' }
},
transformRequest: (data) => JSON.stringify(data),
timeout: 30000, // 请求超时时间
isRepeatRequest: true, // 是否开启重复请求拦截
})
const pending = {}
const CancelToken = axios.CancelToken
const paramsList = ['get', 'delete', 'patch']
const dataList = ['post', 'put']
const isTypeList = (method) => {
if (paramsList.includes(method)) {
return 'params'
} else if (dataList.includes(method)) {
return 'data'
}
}
/**
* 获取请求的key
* @param {object} config
* @param {boolean} isSplice - 是否拼接请求头,请求前需要拼接,请求后不需要拼接
* @returns
*/
const getRequestIdentify = (config, isSplice = false) => {
let url = config.url
if (isSplice) {
url = config.baseURL + config.url
}
const params = { ...(config[isTypeList(config.method)] || {}) }
// t 是随机数,不参与计算唯一值
delete params.t
return encodeURIComponent(url + JSON.stringify(params))
}
/**
* 取消重复
* @param {string} key - 请求唯一url
* @param {boolean} isRequest - 是否执行取消请求
*/
const removePending = (key, isRequest = false) => {
if (pending[key] && isRequest) {
pending[key].cancel('取消重复请求')
}
delete pending[key]
}
service.interceptors.request.use((config) => {
const requestId = getRequestIdentify(config, true)
config.requestId = requestId
// 根据配置是否移除重复请求
config.isRepeatRequest && removePending(requestId, true)
if (!config.cancelToken) {
const source = CancelToken.source()
source.token.cancel = source.cancel
config.cancelToken = source.token
}
// 缓存该请求的取消重复的方法
pending[requestId] = config.cancelToken
return config
})
service.interceptors.response.use((response) => {
// 请求完成,移除缓存
response.config.isRepeatRequest && removePending(response.config.requestId, false)
return response.data
}, (error) => {
if (axios.isCancel(error)) return Promise.reject(error)
// 请求完成,移除缓存
error.config?.isRepeatRequest && removePending(response.config.requestId, false)
return Promise.reject(error)
})
/**
* get请求方法
* @export axios
* @param {string} url - 请求地址
* @param {object} params - 请求参数
* @param {object|undefined|Null} 其他参数
* @returns
*/
export const GET = (url, params, other) => {
params = params || {}
params.t = Date.now()
return service({
url: url,
method: 'GET',
params,
...(other || {})
})
}
通过pending
定义空对象(第16行 ),每次请求前判断拦截是否有重复(第63行 ),有得直接拦截,并返回错误,随后在请求完成时移除请求(第80、88行)。
最终方式
以下将分享一种能够取消重复请求,但能让最新请求作为最终返回,且能多个请求共享状态的方法,关机思路通过对axios.create
实例化的方法多包裹一层Promise
,并在每次请求前记录一些关键参数。
js
// fetchSuper.js
const isTypeList = (method) => {
method = (method || '').toLowerCase()
if (paramsList.includes(method)) {
return 'params'
} else if (dataList.includes(method)) {
return 'data'
}
}
// 用来判断取消请求的标识
const REPEAT_REQUEST_TEXT = 'repeatRequest'
/**
* 包装实际请求动作
* @param {Axios.config} config - 最终合并后的参数
* @param {string} requestId - 请求的唯一值
* @param {Promise.resolve} resolve
* @param {Promise.reject} reject
*/
const defaultAdapterRequest = (config, requestId, resolve, reject) => {
service(config)
.then((response) => {
// 请求成功时,删除缓存,并返回到最上层
delete pending[requestId]
resolve && resolve(response)
})
.catch((error) => {
if (!(axios.isCancel(error) && error.message === REPEAT_REQUEST_TEXT)) {
delete pending[requestId]
reject && reject(error)
}
})
}
/**
* 包装实际请求动作
* @param {Axios.config} config - 最终合并后的参数
* @param {string} requestId - 请求的唯一值
* @param {Promise.resolve} resolve
* @param {Promise.reject} reject
*/
const defaultAdapterRequest = (config, requestId, resolve, reject) => {
service(config)
.then((response) => {
// 请求成功时,删除缓存,并返回到最上层
delete pending[requestId]
resolve && resolve(response)
})
.catch((error) => {
if (!(axios.isCancel(error) && error.message === REPEAT_REQUEST_TEXT)) {
delete pending[requestId]
reject && reject(error)
}
})
}
/**
* 包装请求方法
* @param {Axios.config} config
* @returns
*/
const packService = (config) => {
// 这里为什么不用 webpack.merge 进行深度合并,是因为太消耗性能且一般用不上,普通合并即可
const mergeConfig = Object.assign({}, service.defaults, config)
const requestId = getRequestIdentify(mergeConfig)
mergeConfig.requestId = requestId
if (!mergeConfig.cancelToken) {
const source = CancelToken.source()
source.token.cancel = source.cancel
mergeConfig.cancelToken = source.token
}
// 上传文件或者主动不要重复,则直接请求
if (
!mergeConfig.isRepeatRequest ||
mergeConfig.headers?.['Content-Type'] === 'multipart/form-data;charset=UTF-8'
) {
return service(mergeConfig)
}
// 关键就在这里,如果第一次进来
if (!pending[requestId]) {
pending[requestId] = {}
// 包装多一层Promise,并往缓存存入 cancelToken、resolve、reject、promiseFn
const promiseFn = new Promise((resolve, reject) => {
pending[requestId] = {
cancelToken: mergeConfig.cancelToken,
resolve,
reject
}
defaultAdapterRequest(mergeConfig, requestId, resolve, reject)
})
pending[requestId].promiseFn = promiseFn
return promiseFn
}
// 非第一次进来,则直接取消上一次的请求,并且替换缓存的cancelToken为当前的,否则下一次进来不能正确取消一次的请求
const { cancelToken, resolve, reject, promiseFn } = pending[requestId]
cancelToken.cancel(REPEAT_REQUEST_TEXT)
pending[requestId].cancelToken = mergeConfig.cancelToken
defaultAdapterRequest(mergeConfig, requestId, resolve, reject)
return promiseFn
}
// 最后暴露出去的方法,则改为用packService进行包装
/**
* get请求方法
* @export axios
* @param {string} url - 请求地址
* @param {object} params - 请求参数
* @param {object|undefined|Null} 其他参数
* @returns
*/
export const GET = (url, params, other) => {
params = params || {}
params.t = Date.now()
return packService({
url: url,
method: 'GET',
params,
...(other || {})
})
}
/**
* post请求方法
* @export axios
* @param {string} url - 请求地址
* @param {object} data - 请求参数
* @param {object|undefined|Null} 其他参数
* @returns
*/
export const POST = (url, data = {}, other) => {
return packService({
url,
method: 'POST',
params: { t: Date.now() },
data,
...(other || {})
})
}
defaultAdapterRequest
函数用于包装实际的请求操作。它接收一个配置对象 config
、一个请求的唯一标识 requestId
和两个回调函数 resolve
和 reject
。当请求成功时,从缓存中删除该请求的信息,并通过 resolve
回调返回响应;如果请求失败(但不是因为重复请求被取消),同样从缓存中删除信息并通过 reject
回调传递错误。
packService
是一个关键的函数,用于处理请求前的准备工作。它首先合并了默认配置和传入的配置,然后生成一个请求的唯一标识 requestId
。它会检查是否有相同的请求正在处理中。如果有,它会取消之前的请求并重新发起新的请求;如果没有,则直接发起请求并将相关信息存储在缓存中。
总结
以上是个人经验总结,可能适用你,也可能不适用你,选择一款作为自己项目合适搭配即可。另外,本来是想从Axios
的adapter
下手进行改造,发现最终请求回来并不会走自定义的adapter
方法,看了源码才知道,这只是其中一个环,并不会作为最终返回,故而在外层加多一层Promise
进行拦截,还有在转换请求唯一值(requestId
)可能存在性能问题,如果入参很多,转换效率就会下降,不知道各位道友是否更好的方法。
Demo
- 链接:github.com/MaJiaXuan/a...
- 启动方式:
npm run dev