本文目录导读:

接口重复请求(重复提交)是前端开发中非常常见的问题,主要发生在网络延迟、用户快速点击或支付/订单等敏感操作时,以下是常见的过滤策略,按推荐程度和实现难度排序:
防抖与节流(前端基础方案)
防抖(Debounce)—— 适用于点击提交
原理:用户停止操作后一段时间再执行,如果再次触发则重置计时器。
// 简单防抖实现
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 使用场景:搜索框输入、表单提交
const submitForm = debounce((data) => {
api.submitOrder(data);
}, 500);
节流(Throttle)—— 适用于频繁触发
原理:一段时间内只执行一次。
function throttle(fn, delay = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用场景:按钮点击、滚动事件
const handleClick = throttle(() => {
console.log('click');
}, 1000);
按钮状态锁(最常用、最可靠)
原理:点击后立即禁用按钮,请求完成后恢复。
Vue 示例
<template>
<button :disabled="loading" @click="submit">
{{ loading ? '提交中...' : '提交' }}
</button>
</template>
<script>
export default {
data() {
return { loading: false };
},
methods: {
async submit() {
if (this.loading) return;
this.loading = true;
try {
await api.submitOrder(data);
} finally {
this.loading = false;
}
}
}
};
</script>
React 示例
function SubmitButton() {
const [loading, setLoading] = useState(false);
const handleSubmit = useCallback(async () => {
if (loading) return;
setLoading(true);
try {
await submitOrder(data);
} finally {
setLoading(false);
}
}, [loading]);
return (
<button disabled={loading} onClick={handleSubmit}>
{loading ? '提交中...' : '提交'}
</button>
);
}
请求唯一 ID 去重(后端+前端配合)
原理:每个请求携带唯一ID,后端保存已处理的ID,相同ID返回已有结果。
// 前端生成 UUID
const requestId = uuidv4();
// 发起请求
const response = await fetch('/api/order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId, // 唯一标识
data: { productId: 123 }
})
});
后端处理(伪代码):
# Redis 中存储已处理的 requestId
if redis.exists(request_id):
return {"code": 200, "message": "重复请求", "data": existing_result}
else:
redis.setex(request_id, 3600, "processing") # 1小时过期
# 处理业务逻辑
result = process_order(data)
redis.set(request_id, result)
return result
全局请求拦截器(成熟方案)
使用 axios 或 fetch 拦截器,自动检测和阻止重复请求。
Axios 请求去重(最推荐)
import axios from 'axios';
import md5 from 'md5';
// 存储正在进行的请求
const pendingRequests = new Map();
// 生成请求唯一标识
function generateRequestKey(config) {
return md5(
`${config.method}:${config.url}:${JSON.stringify(config.data || '')}`
);
}
// 请求拦截器
axios.interceptors.request.use((config) => {
const key = generateRequestKey(config);
if (pendingRequests.has(key)) {
// 取消重复请求
const cancel = pendingRequests.get(key);
cancel('Duplicate request cancelled');
// 或者直接返回 null/抛出错误,不给用户重复发送
return Promise.reject(new axios.Cancel('重复请求已被阻止'));
}
// 创建取消令牌
const controller = new AbortController();
config.signal = controller.signal;
pendingRequests.set(key, controller.abort.bind(controller));
return config;
});
// 响应拦截器
axios.interceptors.response.use(
(response) => {
const key = generateRequestKey(response.config);
pendingRequests.delete(key);
return response;
},
(error) => {
if (axios.isCancel(error)) {
console.warn('请求被取消:', error.message);
}
// 确保移除
const config = error.config;
if (config) {
const key = generateRequestKey(config);
pendingRequests.delete(key);
}
return Promise.reject(error);
}
);
// 使用 axios 发起请求时,3 秒内有相同请求,会被自动取消
axios.post('/api/order', { productId: 123 });
封装成 React Hook
function useRequestDebounce() {
const requestQueue = useRef(new Map());
const debouncedRequest = useCallback(async (config) => {
const key = `${config.method}:${config.url}:${JSON.stringify(config.data)}`;
if (requestQueue.current.has(key)) {
console.warn('重复请求已取消:', key);
return;
}
const promise = axios(config).finally(() => {
requestQueue.current.delete(key);
});
requestQueue.current.set(key, promise);
return promise;
}, []);
return { debouncedRequest };
}
// 使用
const { debouncedRequest } = useRequestDebounce();
const handleSubmit = async () => {
const res = await debouncedRequest({
method: 'POST',
url: '/api/order',
data: { productId: 123 }
});
};
Token 机制(幂等性方案)
原理:每次操作前从后端获取一个唯一 token,提交时携带该 token,后端验证 token 是否已使用。
// 1. 获取 token
const { token } = await api.getOperationToken();
// 2. 提交时携带 token
await api.submitOrder({
idempotencyToken: token,
data: { productId: 123 }
});
// 3. 后端验证
function submitOrder(req) {
if redis.get(req.idempotencyToken) {
return { code: 200, message: '已处理' };
}
redis.set(req.idempotencyToken, 'used');
// 处理业务...
}
综合最佳实践(推荐组合)
对于关键业务(支付、下单),建议采用 三层防护:
| 层级 | 方案 | 说明 |
|---|---|---|
| 前端 | 按钮状态锁 + 防抖 | 立即阻止用户重复点击 |
| 前端 | Axios 请求去重 | 防止代码层面重复调用 |
| 后端 | 请求 ID 幂等 | 终极保障,防止网络重试导致的问题 |
// 最终代码示例
function handleSubmit() {
if (this.loading) return; // 按钮锁 1
this.loading = true; // 按钮锁 2
this.throttle(async () => {
try {
// 请求携带 requestId(幂等)
await api.submitOrder({
requestId: generateUUID(),
data: this.formData
});
} finally {
this.loading = false;
}
}, 1000)();
}
注意事项
- 不要完全依赖前端:网络波动、页面刷新等场景下,最终兜底应在后端实现幂等
- 超时处理:长时间未收到响应应允许用户重试
- 用户体验:禁用按钮时要给出明确反馈(加载动画、提示文字)
- 异常恢复:如果请求失败,必须释放锁的状态,允许用户再次操作
根据你的场景复杂度,多数情况下选择 按钮状态锁 + 防抖 即可解决 80% 的问题。