Axios取消重复请求,但能让最新请求作为最终返回,且能共享状态 ,不知小伙您有没有尝到真香~

起因,在项目中发现多个地方同时请求同一个接口且参数相同时,只有最后一个接口能正常返回,并且只有一个地方会展示查询结果...这个问题让我想起了在封装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 和两个回调函数 resolvereject。当请求成功时,从缓存中删除该请求的信息,并通过 resolve 回调返回响应;如果请求失败(但不是因为重复请求被取消),同样从缓存中删除信息并通过 reject 回调传递错误。

packService是一个关键的函数,用于处理请求前的准备工作。它首先合并了默认配置和传入的配置,然后生成一个请求的唯一标识 requestId。它会检查是否有相同的请求正在处理中。如果有,它会取消之前的请求并重新发起新的请求;如果没有,则直接发起请求并将相关信息存储在缓存中。

总结

以上是个人经验总结,可能适用你,也可能不适用你,选择一款作为自己项目合适搭配即可。另外,本来是想从Axiosadapter下手进行改造,发现最终请求回来并不会走自定义的adapter方法,看了源码才知道,这只是其中一个环,并不会作为最终返回,故而在外层加多一层Promise进行拦截,还有在转换请求唯一值(requestId)可能存在性能问题,如果入参很多,转换效率就会下降,不知道各位道友是否更好的方法。

Demo

相关推荐
zgscwxd13 分钟前
thinkphp6模板调用URL方法生成的链接异常
前端·javascript·html
建群新人小猿18 分钟前
退款成功订阅消息点击后提示订单不存在
java·开发语言·前端
y先森1 小时前
js实现导航栏鼠标移入时,下划线跟随鼠标滑动
开发语言·前端·javascript
加德霍克2 小时前
python高级之简单爬虫实现
前端·python·学习
we_前端全家桶3 小时前
小程序中模拟发信息输入框,让textarea可以设置最大宽以及根据输入的内容自动变高的方式
java·前端·小程序
EasyNTS3 小时前
H.265流媒体播放器EasyPlayer.js网页直播/点播播放器WebGL: CONTEXT_LOST_WEBGL错误引发的原因
javascript·webgl·h.265
兔兔爱学习兔兔爱学习4 小时前
leetcode219. Contains Duplicate II
javascript·数据结构·算法
NetX行者4 小时前
基于Vue3与ABP vNext 8.0框架实现耗时业务处理的进度条功能
前端·vue.js·进度条·状态模式