• 作者:老汪软件技巧
  • 发表时间:2024-11-21 15:03
  • 浏览量:

起因,在项目中发现多个地方同时请求同一个接口且参数相同时,只有最后一个接口能正常返回,并且只有一个地方会展示查询结果...这个问题让我想起了在封装Axios时,参考网上的例子直接抄来用。

错误方式

以上这种情况发出了8个相同的请求,前7个被取消了,只有最后一个生效,这种会导致前面的请求都报错,得不到想要的结果,先来分析一下以上的实现原理,下面贴出关键代码。

// 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,并在每次请求前记录一些关键参数。

// 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