本文目录导读:

接口重复请求(重复提交)是前端开发中非常常见的问题,主要发生在网络延迟、用户快速点击或支付/订单等敏感操作时,以下是常见的过滤策略,按推荐程度和实现难度排序:
防抖与节流(前端基础方案)
防抖(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% 的问题。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播