在前端开发中,防止页面重复请求(如重复提交表单、重复调用接口等)是提升用户体验和系统稳定性的关键。以下是 多种防重复请求的方案,涵盖不同场景(表单提交、接口调用、按钮点击等),并提供代码示例和最佳实践。
1. 按钮禁用(适用于表单提交)
实现思路
在表单提交时禁用按钮,防止用户重复点击。
代码示例
html
<button id="submitBtn" onclick="submitForm()">提交</button>
<script>
function submitForm() {
const btn = document.getElementById('submitBtn');
btn.disabled = true; // 禁用按钮
btn.textContent = '提交中...'; // 可选:修改按钮文本
// 模拟异步请求(如 fetch/axios)
setTimeout(() => {
console.log('表单提交成功');
btn.disabled = false; // 请求完成后恢复按钮
btn.textContent = '提交';
}, 2000);
}
</script>
关键点
- 简单有效:适用于大多数表单场景。
- 用户体验:可添加加载状态提示(如按钮文本变化)。
- 恢复时机:在请求成功或失败后恢复按钮状态。
2. 防抖(Debounce)与节流(Throttle)
适用场景
- 防抖:连续快速触发时,只执行最后一次(如搜索框输入)。
- 节流:固定时间间隔内只执行一次(如滚动事件、按钮连续点击)。
代码示例
1. 防抖(Debounce)
javascript
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用示例:搜索框输入
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(() => {
console.log('触发搜索请求');
}, 500));
2. 节流(Throttle)
javascript
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 使用示例:按钮连续点击
const btn = document.getElementById('btn');
btn.addEventListener('click', throttle(() => {
console.log('按钮点击');
}, 1000));
关键点
- 防抖:适合输入框、搜索等延迟触发场景。
- 节流:适合高频触发事件(如滚动、缩放)。
- 库支持:可使用 Lodash 的 _.debounce 和 _.throttle。
3. 请求锁(全局状态管理)
实现思路
通过全局变量或状态管理工具(如 Vuex、Redux)标记请求状态,防止重复发起。
代码示例
1. 使用全局变量
javascript
let isRequesting = false;
function fetchData() {
if (isRequesting) return; // 如果正在请求,直接返回
isRequesting = true; // 标记为请求中
fetch('/api/data')
.then(() => console.log('请求成功'))
.catch(() => console.log('请求失败'))
.finally(() => {
isRequesting = false; // 请求完成后重置状态
});
}
2. 使用 Vuex(Vue 项目)
javascript
// store.js
const store = new Vuex.Store({
state: { isLoading: false },
mutations: {
setLoading(state, payload) {
state.isLoading = payload;
},
},
});
// 组件中
methods: {
fetchData() {
if (this.$store.state.isLoading) return;
this.$store.commit('setLoading', true);
fetch('/api/data')
.then(() => console.log('请求成功'))
.catch(() => console.log('请求失败'))
.finally(() => {
this.$store.commit('setLoading', false);
});
},
},
关键点
- 全局管理:适合复杂项目,避免重复请求冲突。
- 状态复用:可结合加载动画、按钮禁用等场景。
- 扩展性:可记录请求队列(如多个接口依赖)。
4. Token 或请求标识(幂等性设计)
实现思路
为每个请求生成唯一标识(如 UUID),后端通过标识判断是否重复请求。
代码示例
1. 前端生成请求 ID
javascript
function generateRequestId() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
function fetchData() {
const requestId = generateRequestId(); // 生成唯一 ID
const headers = { 'X-Request-ID': requestId };
fetch('/api/data', { headers })
.then((res) => res.json())
.then((data) => console.log(data));
}
2. 后端处理(Node.js 示例)
javascript
const requestCache = new Set(); // 简单缓存(实际项目用 Redis)
app.post('/api/data', (req, res) => {
const requestId = req.headers['x-request-id'];
if (requestCache.has(requestId)) {
return res.status(400).json({ error: '重复请求' });
}
requestCache.add(requestId); // 记录请求 ID
// 处理业务逻辑...
});
关键点
- 幂等性:确保重复请求不会产生副作用(如重复扣款)。
- 后端支持:需要后端配合实现标识校验。
- 缓存策略:实际项目可用 Redis 存储请求 ID,设置过期时间。
5. Cancel Token(取消重复请求)
适用场景
使用 axios 或 fetch 时,取消未完成的重复请求。
代码示例
1. Axios 的 CancelToken
javascript
let cancelTokenSource = null;
function fetchData() {
// 取消之前的请求
if (cancelTokenSource) {
cancelTokenSource.cancel('取消重复请求');
}
cancelTokenSource = axios.CancelToken.source();
axios.get('/api/data', {
cancelToken: cancelTokenSource.token,
})
.then((res) => console.log(res.data))
.catch((err) => {
if (!axios.isCancel(err)) {
console.log('请求失败:', err);
}
});
}
2. Fetch 的 AbortController(现代浏览器支持)
javascript
let controller = null;
function fetchData() {
// 取消之前的请求
if (controller) {
controller.abort();
}
controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name !== 'AbortError') {
console.log('请求失败:', err);
}
});
}
关键点
- 主动取消:适合用户快速切换页面或输入时取消旧请求。
- 兼容性:AbortController 是现代标准,IE 不支持。
- 资源释放:避免不必要的网络请求和内存占用。
6. 表单提交的双重验证
实现思路
结合前端验证和后端校验,防止重复提交。
代码示例
1. 前端验证(唯一标识)
html
<form id="myForm">
<input type="hidden" name="formToken" value="<%= generateToken() %>" />
<!-- 其他表单字段 -->
<button type="submit">提交</button>
</form>
<script>
document.getElementById('myForm').addEventListener('submit', (e) => {
e.preventDefault();
const btn = e.submitter;
btn.disabled = true; // 禁用按钮
const formData = new FormData(e.target);
fetch('/api/submit', {
method: 'POST',
body: formData,
})
.then(() => console.log('提交成功'))
.catch(() => console.log('提交失败'))
.finally(() => {
btn.disabled = false;
});
});
</script>
2. 后端验证(Node.js 示例)
javascript
const submittedTokens = new Set(); // 存储已提交的 Token
app.post('/api/submit', (req, res) => {
const formToken = req.body.formToken;
if (submittedTokens.has(formToken)) {
return res.status(400).json({ error: '重复提交' });
}
submittedTokens.add(formToken); // 记录 Token
// 处理业务逻辑...
});
关键点
- 前后端结合:前端禁用按钮 + 后端 Token 校验。
- 安全性:Token 应随机生成且不可预测。
- 过期处理:Token 可设置过期时间(如 5 分钟)。
7. 最佳实践建议
- 表单提交:按钮禁用 + 后端 Token 校验。
- 搜索输入:防抖(如 500ms 后触发)。
- 连续点击:节流(如 1s 内只允许一次)。
- 复杂项目:结合请求锁(Vuex/Redux)和 Cancel Token。
- 高并发场景:后端实现幂等性设计(如 Token + 数据库唯一约束)。