前言
有一天老板突然要求:前端要在全局层面防止接口被重复请求。
原因很简单------有些用户点点点,疯狂点击按钮,就能薅到优惠券或者触发一些逻辑 bug。
其实最稳妥的办法应该是在 服务端限制,但老板偏偏要前端做。那没办法,咱就来研究下前端怎么搞。
因为老项目接口多如牛毛,一个个排查加 loading 不现实,只能来一套全局拦截方案。
方案一:全屏 Loading
最容易想到的方案:
- 在 请求拦截器里加一个全屏 Loading(转圈圈)。
- 在 响应拦截器里关掉 Loading。
这样,用户点第二次的时候,看到 Loading 就不会继续点了。
缺点:
- 很粗暴,体验不友好。
- 有的接口自己本身就有局部 Loading,这么一套就会变成"双转圈",头皮发麻。
总结:能用,但很丑。
方案二:拦截相同请求,不让发出去
换个思路:
如果两个请求完全一模一样,那就拦掉后面的,不让它们发到后端。
关键点: 一个请求可以用这些东西来唯一标识:
- 请求方法(GET/POST)
- URL
- 参数(params / data)
- 页面地址(hash 或 pathname)
于是我们就写了个函数,把这些信息拼成一个 key:
js
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join("&");
}
思路很简单:
- 请求前,把 key 存到一个集合里。
- 如果下一个请求的 key 已经存在,说明它是重复的 → 拦掉。
- 等第一个请求返回了,就把 key 从集合里移除。
问题来了:
- 如果多个请求被拦掉,但后续逻辑里有错误处理,就会弹出一堆报错提示(用户体验极差)。
- 如果同一个页面的两个组件都要调同一个接口,后发的请求就拿不到数据了 → 页面挂了。
总结:思路没错,但坑很多。
方案三:挂起相同请求,结果共享(最终方案)
那我们能不能这样:
- 第一个请求照常发出去。
- 后面相同的请求,不是真的发,而是先挂起。
- 等第一个请求返回结果(成功/失败),再把结果共享给这些挂起的请求。
这样:
- 不会重复打接口。
- 后续的请求也能拿到结果。
- 用户体验正常。
js
import axios from "axios";
// 创建 axios 实例
let instance = axios.create({
baseURL: "/api/" // 这里你可以换成自己的接口前缀
});
/**
* 发布订阅类(EventEmitter)
* - 用来存放"挂起的请求"
* - 第一个请求成功/失败时,把结果广播给挂起的请求
*/
class EventEmitter {
constructor() {
this.event = {}; // 用来存放事件和对应的回调函数
}
// 订阅(挂起的请求)
on(type, cbResolve, cbReject) {
if (!this.event[type]) {
this.event[type] = [[cbResolve, cbReject]];
} else {
this.event[type].push([cbResolve, cbReject]);
}
}
// 发布(当请求返回时,把结果广播给所有订阅者)
emit(type, res, ansType) {
if (!this.event[type]) return;
this.event[type].forEach(cbArr => {
if (ansType === "resolve") {
cbArr[0](res); // 通知成功的订阅者
} else {
cbArr[1](res); // 通知失败的订阅者
}
});
}
}
实现方式:发布订阅模式
我们写了一个简单的 EventEmitter:
on
→ 把"挂起的请求"登记到订阅列表里。emit
→ 第一个请求返回时,把结果广播给订阅者。
在请求拦截器里:
- 如果发现请求 key 已经存在,就挂起它,等结果回来。
- 如果是新请求,就发出去。
在响应拦截器里:
- 请求完成时,通知所有订阅的挂起请求,返回结果。
👉 完整代码在 Demo 地址。
特殊情况:文件上传
写完以为万事大吉,结果发现上传文件的时候有 bug:
- 我传两个不同文件,但接口只发了一次。
原因是:
- 请求体里的
FormData
被JSON.stringify
转成了{}
。 - 所以不同的文件生成的 key 是一样的,被误判成重复请求。
解决办法: 在生成 key 时,先判断是不是文件上传:
js
function isFileUploadApi(config) {
return Object.prototype.toString.call(config.data) === "[object FormData]";
}
如果是文件上传,就直接放行,不再做重复请求拦截。
总结
三种方案对比:
方案 | 思路 | 优点 | 缺点 |
---|---|---|---|
方案一 | 全局 Loading | 简单 | 粗暴、体验差 |
方案二 | 相同请求直接拦截 | 节省接口 | 容易误伤,逻辑混乱 |
方案三 | 相同请求挂起 + 结果共享 | 最优解,体验好 | 代码复杂,需要维护 |
最终采用的是 方案三(挂起相同请求,结果共享),再配合文件上传的特殊处理,基本覆盖了线上场景。
这样一来,不用逐个接口改代码,省心省力,老板也满意。
后记
这个功能看似是"前端优化",但本质上还是 前端兜底,真正要防止薅羊毛,还是得后端加限流和幂等处理。
前端这套更像是:
- 避免重复请求浪费资源
- 提升用户体验
- 减少一些潜在的 bug