接口请求相关

文章介绍了前端处理HTTP请求时的一些常见问题和解决方案,包括如何使用axios的CancelToken取消重复请求,限制并发请求的数量以及在Token失效时无感刷新并重新发送失败请求的策略。同时提到了实现可停止的轮询机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.取消重复请求

场景:用户连续的点击按钮,如果按钮未做控制,将不断的向后端发起重复的请求,(如果是新增内容或新增订单将产生两条)

思路:收集正在请求中的接口,存储在队列中,如果相同的接口再次被触发,则直接取消正在请求中的接口并从队列中删除,然后再重新发起请求并储存在队列中。如果接口返回了结果,就从队列中删除。

问题:

  1. 如何取消正在请求中的接口?
  2. 怎么判断是重复的接口?

如何取消正在请求中的接口

axios是基于XMLHttpRequest对象进行封装的,所以我们要看XMLHttpRequest中的abort()方法,该方法就是用于中止正在请求中的接口。axios中封装的取消正在请求中的接口方法为:CancelToken

let CancelToken = axios.CancelToken;
let cancel = '';
axios.get('/', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
})

// 取消请求
cancel()

判断重复请求并放入存储队列中

这里规定:请求方法、请求方式、请求参数都一样的情况下,我们就认为是重复的请求

存储队列使用Map,使用键值对存储请求 

const pendingMap = new Map();

// 生成唯一的key
const getPendingKey = (config) => {
    let { url, method, params, data } = config;
    if (typeof data === 'string') data = JSON.parse(data)
    return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}

// 储存每个请求的cancel方法
const addPending = config => {
    const pendingKey = getPendingKey(config);
    config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
        if (!pendingMap.has(pendingKey)) {
            pendingMap.set(pendingKey, cancel)
        }
    })
}

// 取消重复请求 并在队列中删除
const removePending = config => {
    const pendingKey = getPendingKey(config);
    if (pendingMap.has(pendingKey)) {
        const cancelToken = pendingMap.get(pendingKey);
        cancelToken(pendingKey);
        pendingMap.delete(pendingKey);
    }
}

// 错误处理
const httpErrorStatusHandle = (error) => {
    let message = ''
    // error.message就是cancelToken传过来的值
    if (axios.isCancel(error)) return console.log('重复请求取消了' + error.message)
    if (error && error.response) {
        switch (error.response.status) {
            case 302: message = '接口重定向了!'; break;
            case 400: message = '参数不正确!'; break;
            case 401: message = '您未登录,或者登录已经超时,请先登录!'; break;
            case 403: message = '您没有权限操作!'; break;
            case 404: message = `请求地址出错: ${error.response.config.url}`; break; // 在正确域名下
            case 408: message = '请求超时!'; break;
            case 409: message = '系统已存在相同数据!'; break;
            case 500: message = '服务器内部错误!'; break;
            case 501: message = '服务未实现!'; break;
            case 502: message = '网关错误!'; break;
            case 503: message = '服务不可用!'; break;
            case 504: message = '服务暂时无法访问,请稍后再试!'; break;
            case 505: message = 'HTTP版本不受支持!'; break;
            default: message = '异常问题,请联系管理员!'; break

        }
    }
    if (error.message.includes('timeout')) message = '网络请求超时!';
    if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常!' : '您断网了!';


}

// 响应数据处理
const httpResponseData = (response) => {
    // 这里可以具体做一些code的判断处理
    return response.data
}

/**
 * 
 * @param {*} axiosConfig 
 * @param {*} customOptions { 取消重复请求:repeat_request_cancel, 展示错误信息error_message_show }
 * @returns 
 */
// axios配置
function myAxios(axiosConfig, customOptions) {
    const service = axios.create({
        baseUrl: '',
        timeout: 10000
    })

    // 自定义配置是否需要取消重复请求的接口
    let custom_options = Object.assign({ repeat_request_cancel: true, error_message_show: true }, customOptions)
    // 请求拦截
    service.interceptors.request.use(
        config => {
            // 处理请求头信息
            removePending(config)
            custom_options.repeat_request_cancel && addPending(config)
            return config
        },
        error => Promise.reject(error)
    );
    // 响应拦截
    service.interceptors.response.use(
        response => {
            removePending(response.config);
            return httpResponseData(response)
        },
        error => {
            error.config && removePending(error.config);
            // 处理错误状态码
            custom_options.error_message_show && httpErrorStatusHandle(error)
            return Promise.reject(error)
        }
    )
    return service(axiosConfig)
}

axios通过CancelToken取消重复的请求 只是前端被取消了, 但是后端仍然会接收到被取消的请求,所以应该后端也做相应的控制 或者前端使用loading去显示用户的重复点击触发

2.控制并发请求的数量

返回promise 初始只发送maxNum个请求,当一个请求返回结果后,开始下一个请求,直到请求结束。

/**
 * 串行并行最大数
 * 一次最多发送maxNum的请求,请求结果和url的顺序一致
 */
function multiRequest(urls=[], maxNum = 5) {
    const len = urls.length;
    // 创建存储结果
    let results = new Array(len).fill(false)
    // 当前完成的数量
    let count = 0;
    return new Promise((resolve, reject) => {
        while(count < maxNum) {
            next()
        }
    
        function next() {
            let current = count++;
            if (current >= len) {
                !results.includes(false) && resolve(results)
                return
            }
            const url = urls[current];
            fetch(url).then(res => {
                results[current] = res;
                if (current < len) {
                    next()
                }
            }).catch(err => {
                results[current] = err;
                if (current < len) {
                    next()
                }
            })
        }
    })

}

3.无感刷新token

当token失效后,需要使用refresh_token去刷新请求,当刷新完请求之后,要将同一时间因为token失效而请求失败的请求再次发送。

思路:定义一个isRefreshing = false标志,当刷新请求时 isRefreshing = true

定义一个储存刷新token期间返回的401接口的数组,requestList = [],当刷新token后重新发起这些失败的请求。

const request = axios.create({
  baseURL: '',
  timeout: 10000
})

// 请求拦截器
request.interceptors.request.use(
  config => {
    // 统一设置用户身份 Token
    const token = 'xxx'
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
// 控制刷新token的状态
let isRereshing = false
// 存储刷新token期间过来的401请求
let requestList: any[] = []

// 响应拦截器
request.interceptors.response.use(
  response => {
    const { code, result, message } = response.data
    // 状态为2xx都会进入
    if (code === ResultEnum.SUCCESS) {
      return result
    } else {
      // 做其他错误响应
      ElMessage.error(`错误信息: ${message as string}`)
      return Promise.reject(response.data)
    }
  },
  async (err) => {
    // 超出2状态码的进入这里

    if (err.response) {
      // 请求发出并收到响应
      const { status } = err.response
      if (status === 400) {
        ElMessage.error('请求参数错误')
      } else if (status === 401) {
        const usertore = useUserStore()
        // token无效
        if (!usertore.userInfo.id) {
          // 没有登录 退出到登录页
          redirectLogin()
          return Promise.reject(err)
        }

        // 已登录, token过期 刷新token
        if (!isRereshing) {
          isRereshing = true
          return axios.post(url,{ refreshToken: usertore.userInfo?.refreshToken ?? '' }).then(res => {
            if (res.data.code !== '0000') {
              throw new Error('刷新 Token 失败')
            }
            // 刷新成功
            usertore.userInfo.token = res.data.data.token
            usertore.userInfo.refreshToken = res.data.data.refreskToken
            requestList.forEach(cb => cb())
            requestList = []
            return request(err.config)
          }).catch(e => {
            usertore.userInfo = {
              id: '',
              userName: '',
              roleId: '',
              roleName: ''
            }
            redirectLogin()
            return Promise.reject(e)
          }).finally(() => {
            isRereshing = false
          })
        }

        // 正在刷新中
        return new Promise(resolve => {
          requestList.push(() => {
            resolve(request(err.config))
          })
        })
      } else if (status === 403) {
        ElMessage.error('没有权限,请联系管理员')
      } else if (status === 404) {
        ElMessage.error('请求的资源不存在')
      } else if (status > 500) {
        ElMessage.error('服务端错误,请联系管理员')
      } else {
        ElMessage.error('请求失败')
      }
    } else if (err.request) {
      ElMessage.error('请求超时,请刷新重试')
    } else {
      ElMessage.error('请求失败')
    }
    return Promise.reject(err)
  }
)

4.实现可停止的轮询

function myInterval(callback, interbal = 3000) {
    let timer = null;
    let isStop = false;
    const stop = () => {
        isStop = true
        clearTimeout(timer)
    }
    const start = async () => {
        isStop = false
        await loop()
    }

    const loop = async () => {
        try {
            await callback(stop)
            if (isStop) return
            return timer = setTimeout(loop, interbal)
        } catch (e) {
            stop()
            throw new Error('轮询出错')
        }
        
    }
}

function main(stop) {
    // 可调用stop停止轮询
}

const intervalManager = myInterval(main)

intervalManager.start()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值