如何防止接口重复请求

221 阅读2分钟

方案一:请求队列

拦截相同请求:对于相同的请求,先挂起,等最先发出去的请求拿到数据后,把成功或失败的结果共享给后面的相同请求。

  • 拿到结果后,返回给之前挂起的请求,使用发布订阅模式
  • 挂起的请求,需要拦截,所以在请求拦截器中通过return Promise.reject()直接中断请求,做特殊标记,便于在响应拦截器中处理。
import axios from 'axios'

let instance = axios.create({
  baseUrl: '/api/',
})

//发布订阅

class EventEmitter {
  constructor() {
    this.event = {}
  }
  on(type, cbres, cbrej) {
    if (!this.event[type]) {
      this.event[type] = [[cbres, cbrej]]
    } else {
      this.event[type].push([cbres, cbrej])
    }
  }
  emit(type, res, ansType) {
    if (!this.event[type]) return
    else {
      this.event[type].forEach((cbArr) => {
        if (ansType === 'resolve') {
          cbArr[0](res)
        } else {
          cbArr[1](res)
        }
      })
    }
  }
}

//根据请求生成对应的key

function generateReqKey(config, hash) {
  const { method, url, params, data } = config
  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
    '&'
  )
}

// 对于请求体data中的数据是FormData类型的处理
function isFileUploadApi(config) {
  return Object.prototype.toString.call(config.data) === '[object FormData]'
}

// 存储已发送但未响应的请求
const pendingRequest = new Set()

// 发布订阅容器
const ev = new EventEmitter()

//添加请求拦截器

instance.interceptors.request.use(
  async (config) => {
    let hash = location.hash
    //生成请求key
    let reqKey = generateReqKey(config, hash)

    if (!isFileUploadApi(config) && pendingRequest.has(reqKey)) {
      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
      let res = null
      try {
        // 接口成功响应
        res = await new Promise((resolve, reject) => {
          ev.on(reqKey, resolve, reject)
        })
        return Promise.reject({
          type: 'limiteResSuccess',
          val: res,
        })
      } catch (limitFunErr) {
        // 接口报错
        return Promise.reject({
          type: 'limiteResError',
          val: limitFunErr,
        })
      }
    } else {
      // 将请求的key保存在config
      config.pendKey = reqKey
      pendingRequest.add(reqKey)
    }

    return config
  },
  function (error) {
    return Promise.reject(error)
  }
)

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 将拿到的结果发布给其他相同的接口
    handleSuccessResponse_limit(response)
    return response
  },
  function (error) {
    return handleErrorResponse_limit(error)
  }
)

// 接口响应成功
function handleSuccessResponse_limit(response) {
  const reqKey = response.config.pendKey
  if (pendingRequest.has(reqKey)) {
    let x = null
    try {
      x = JSON.parse(JSON.stringify(response))
    } catch (e) {
      x = response
    }
    pendingRequest.delete(reqKey)
    ev.emit(reqKey, x, 'resolve')
    delete ev.event[reqKey]
  }
}

// 接口走失败响应
function handleErrorResponse_limit(error) {
  if (error.type && error.type === 'limiteResSuccess') {
    return Promise.resolve(error.val)
  } else if (error.type && error.type === 'limiteResError') {
    return Promise.reject(error.val)
  } else {
    const reqKey = error.config.pendKey
    if (pendingRequest.has(reqKey)) {
      let x = null
      try {
        x = JSON.parse(JSON.stringify(error))
      } catch (e) {
        x = error
      }
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'reject')
      delete ev.event[reqKey]
    }
  }
  return Promise.reject(error)
}

export default instance

方案二:利用axios提供的cancelToken

通过axios请求拦截器,在每次请求前把请求信息和请求的取消方法放到一个map对象当中,并且判断map对象当中是否已经存在该请求信息的请求,如果存在取消上次请求

方案三:使用状态管理库

vue中,可以结合状态管理库管理请求状态,确保只有一个请求在进行,避免重复请求。