编程 前端接口防止重复请求的实现方案,主要通过axios拦截器来处理请求和响应

2024-11-19 08:37:02 +0800 CST views 560

前端接口防止重复请求实现方案

前言

前段时间,老板要求我们前端组对整个项目进行接口防止重复请求的处理(原因是有用户通过快速点击薅到了一些优惠券)。当时第一反应是,这种防止薅羊毛的方案在服务端加限制最保险,前端的限制毕竟能拦截的有限。但既然老板要求,那我们就搞一下吧。虽然大部分接口处理都加了 loading,但不能确保每个接口都加了。如果要一个个接口排查维护,这个已经有四五年历史的系统,成百上千的接口排查起来太费精力,所以我们决定通过全局处理。以下是我们这次防重复请求的实现方案总结。

方案一

最容易想到的方案是使用 axios 拦截器,在请求拦截器中开启全屏 Loading,然后在响应拦截器中将 Loading 关闭。

// 代码示例
axios.interceptors.request.use(config => {
  showLoading();
  return config;
}, error => {
  hideLoading();
  return Promise.reject(error);
});

axios.interceptors.response.use(response => {
  hideLoading();
  return response;
}, error => {
  hideLoading();
  return Promise.reject(error);
});

这个方案虽然可以满足需求,但直接搞全屏 Loading 并不太美观,何况在项目中有一些局部 Loading,会出现 Loading 套 Loading 的情况,看起来不友好。

方案二

考虑到全屏 Loading 方案不太友好,针对同一个接口,如果传参相同,一般来说没有必要连续请求多次。所以我们可以通过代码逻辑直接拦截掉相同的请求,不让它到达服务端。

首先,我们要确定什么样的请求算相同请求:

一个请求包含的内容不外乎是请求方法、地址、参数以及请求发出的页面 hash。这些数据组合起来生成一个 key 作为请求的标识。

// 根据请求生成对应的key
function generateReqKey(config, hash) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}

有了请求的 key,就可以在请求拦截器中收集每次发起的请求,如果有相同的请求进来,就在集合中比对,如果已经存在,就拦截掉。当请求完成响应后,再将请求从集合中移除。

// 代码示例
const pendingRequest = new Set();

axios.interceptors.request.use(config => {
  const hash = location.hash;
  const reqKey = generateReqKey(config, hash);

  if (pendingRequest.has(reqKey)) {
    return Promise.reject(new Error('重复请求'));
  }

  config.pendKey = reqKey;
  pendingRequest.add(reqKey);
  return config;
}, error => Promise.reject(error));

axios.interceptors.response.use(response => {
  const reqKey = response.config.pendKey;
  pendingRequest.delete(reqKey);
  return response;
}, error => {
  const reqKey = error.config.pendKey;
  pendingRequest.delete(reqKey);
  return Promise.reject(error);
});

这个方案看起来还不错,但它可能会引发更多问题,比如错误提示可能重复,或者多组件同时调用同一接口时的逻辑错误。

方案三

我们最终采用的方案是延续方案二的思路,但这次我们对相同的请求进行挂起,等最先发出去的请求拿到结果后,将结果共享给后续相同的请求。

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
    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("&");
}

const pendingRequest = new Set();
const ev = new EventEmitter()

// 请求拦截器
instance.interceptors.request.use(async (config) => {
  let hash = location.hash
  let reqKey = generateReqKey(config, hash)
  
  if(pendingRequest.has(reqKey)) {
    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 {
    config.pendKey = reqKey
    pendingRequest.add(reqKey)
  }
  return config;
}, error => Promise.reject(error));

// 响应拦截器
instance.interceptors.response.use(response => {
  handleSuccessResponse_limit(response)
  return response;
}, error => handleErrorResponse_limit(error));

// 处理成功响应
function handleSuccessResponse_limit(response) {
  const reqKey = response.config.pendKey
  if(pendingRequest.has(reqKey)) {
    let x = JSON.parse(JSON.stringify(response))
    pendingRequest.delete(reqKey)
    ev.emit(reqKey, x, 'resolve')
  }
}

// 处理错误响应
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 = JSON.parse(JSON.stringify(error))
      pendingRequest.delete(reqKey)
      ev.emit(reqKey, x, 'reject')
    }
    return Promise.reject(error);
  }
}

export default instance;

补充

到这里,我们的逻辑已经基本完善,但线上情况复杂多样,尤其是文件上传的场景。

function isFileUploadApi(config) {
  return Object.prototype.toString.call(config.data) === "[object FormData]"
}

在生成请求 key 时需要注意请求体的数据类型,如果是 FormData 类型的数据直接放行。

最后

通过这种全局拦截的方法,我们实现了前端接口防止重复请求的功能,不需要一个个接口改代码,就可以愉快地打代码啦!

复制全文 生成海报 前端开发 接口设计 性能优化

推荐文章

浏览器自动播放策略
2024-11-19 08:54:41 +0800 CST
Vue3的虚拟DOM是如何提高性能的?
2024-11-18 22:12:20 +0800 CST
Java环境中使用Elasticsearch
2024-11-18 22:46:32 +0800 CST
用 Rust 玩转 Google Sheets API
2024-11-19 02:36:20 +0800 CST
PHP设计模式:单例模式
2024-11-18 18:31:43 +0800 CST
PyMySQL - Python中非常有用的库
2024-11-18 14:43:28 +0800 CST
Python 获取网络时间和本地时间
2024-11-18 21:53:35 +0800 CST
CSS 实现金额数字滚动效果
2024-11-19 09:17:15 +0800 CST
使用Rust进行跨平台GUI开发
2024-11-18 20:51:20 +0800 CST
利用图片实现网站的加载速度
2024-11-18 12:29:31 +0800 CST
windows下mysql使用source导入数据
2024-11-17 05:03:50 +0800 CST
php使用文件锁解决少量并发问题
2024-11-17 05:07:57 +0800 CST
软件定制开发流程
2024-11-19 05:52:28 +0800 CST
Nginx 性能优化有这篇就够了!
2024-11-19 01:57:41 +0800 CST
Rust async/await 异步运行时
2024-11-18 19:04:17 +0800 CST
内网穿透技术详解与工具对比
2025-04-01 22:12:02 +0800 CST
PHP服务器直传阿里云OSS
2024-11-18 19:04:44 +0800 CST
10个极其有用的前端库
2024-11-19 09:41:20 +0800 CST
程序员茄子在线接单