在使用XHR(XMLHttpRequest)或现代的Fetch API进行网络请求时,确保用户不重复提交数据是一个常见的需求,特别是在表单提交或任何需要用户确认的操作中。这里有几个策略可以帮助你避免数据重复提交:
1. 禁用提交按钮
一旦用户点击提交按钮,就立即禁用该按钮。这可以防止用户多次点击。
document.querySelector('#submitButton').addEventListener('click', function(event) {
this.disabled = true; // 禁用按钮
// 后续是XHR或Fetch请求的代码
});
2. 标记请求状态
使用一个标志(比如布尔值)来追踪请求的状态。在请求开始时设置标志为
true
,在请求完成时(无论是成功还是失败)设置回
false
。在请求发送前检查这个标志,如果已经是
true
,则不发送请求。
let isRequesting = false;
function sendRequest() {
if (isRequesting) {
console.log('请求已经在处理中,请稍候...');
return;
}
isRequesting = true;
// 发送XHR或Fetch请求
// ...
// 请求结束后
isRequesting = false;
}
// 绑定到按钮点击事件或其他触发逻辑
3. 使用Token或Nonce
在表单提交时,生成一个唯一的Token或Nonce,并将其与表单数据一起发送到服务器。服务器检查这个Token是否已经被使用(例如,通过将其存储在数据库或缓存中)。如果Token已存在,则拒绝请求。
客户端:
let token = generateUniqueToken(); // 自定义函数生成唯一Token
// 发送包含token的请求
服务器端:
# 伪代码
if token_exists_in_database(token):
return "Error: Token already used"
else:
# 处理请求
save_token_to_database(token)
4. 客户端和服务器双重检查
结合上述方法,即在客户端禁用按钮并设置请求状态标志,同时在服务器端通过Token检查确保请求的唯一性。这样可以提供更强的保护,防止因客户端脚本错误或绕过导致的重复提交。
5. 使用防抖(Debouncing)或节流(Throttling)
虽然这主要用于控制事件处理函数的触发频率,但在某些情况下,也可以用来防止快速重复提交。不过,对于标准的表单提交来说,这通常不是首选方法。
总结
确保用户不重复提交数据通常需要结合客户端和服务器端的策略。禁用提交按钮和使用请求状态标志是两种简单而有效的客户端方法,而服务器端Token检查则提供了额外的保护。选择哪种方法或它们的组合,取决于你的具体需求和应用场景。
---------------------------------------------------下axios--------------------------------------------------------------------
在使用axios进行网络请求时,确保用户不重复提交数据也可以采用类似上述提到的策略。axios本身是一个基于Promise的HTTP客户端,用于浏览器和node.js,但它不直接提供防止重复提交的内置机制。不过,你可以通过以下几种方式来实现:
1. 禁用提交按钮
这是最简单也是最直接的方法。当用户点击提交按钮时,立即禁用该按钮,防止用户再次点击。
document.getElementById('submitButton').addEventListener('click', function(event) {
this.disabled = true; // 禁用按钮
axios.post('/your-endpoint', {
// 你的数据
})
.then(response => {
// 处理成功响应
})
.catch(error => {
// 处理错误
// 注意:在这里你可能需要重新启用按钮,以便用户可以尝试重新提交
// 但这通常取决于你的应用逻辑
})
.finally(() => {
// 无论成功或失败,都会执行
// 这里也可以重新启用按钮,但通常是在用户看到结果后
});
});
2. 使用请求状态标志
你可以定义一个标志变量来跟踪请求的状态,确保在请求完成之前不会发送新的请求。
let isSubmitting = false;
function submitForm() {
if (isSubmitting) {
console.log('请求已经在处理中,请稍候...');
return;
}
isSubmitting = true;
axios.post('/your-endpoint', {
// 你的数据
})
.then(response => {
// 处理响应
})
.catch(error => {
// 处理错误
})
.finally(() => {
isSubmitting = false; // 请求完成,无论是成功还是失败
});
}
// 绑定submitForm到适当的事件监听器上
3. 结合Token或Nonce
这种方法涉及到在服务器端和客户端之间共享一个唯一的标识符(Token或Nonce)。每次表单提交时,都会生成一个新的Token,并将其与表单数据一起发送到服务器。服务器检查Token的有效性,并在验证后将其标记为已使用。
- 客户端:生成Token,发送到服务器。
- 服务器:验证Token,处理请求,将Token标记为已使用。
注意:这种方法需要服务器端的支持,并且你需要维护一个Token的存储系统(可能是数据库、缓存或内存中的数据结构)。
4. 使用防抖(Debouncing)或节流(Throttling)
虽然这些方法主要用于控制事件处理函数的触发频率,但在某些情况下,你也可以考虑使用它们来限制请求的频率。然而,对于防止表单的重复提交来说,它们可能不是最直接或最有效的解决方案。
总结
在大多数情况下,结合使用禁用提交按钮和请求状态标志的方法将足以满足防止axios请求重复提交的需求。如果你需要更高级的控制,比如跨会话的Token验证,那么你将需要实现更复杂的服务器端逻辑。
import axios from 'axios'
import { Notification, MessageBox, Message, Loading } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from "@/utils/ruoyi";
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import qs from 'qs'
let downloadLoadingInstance;
// 是否显示重新登录
export let isRelogin = { show: false };
// axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 对应国际化资源文件后缀
axios.defaults.headers['Content-Language'] = 'zh_CN'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_PY,
// 超时
timeout: 10000
})
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
//此处以下为重点
//headers中的content-type 默认的大多数情况是 application/json,就是json序列化的格式
config.headers['Content-Type'] = 'application/json'
//为了判断是否为formdata格式,增加了一个变量为type,如果type存在,而且是form的话,则代表是formData的格式
if (config.type && config.type === 'form') {
config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
//data是接收的数据,接收的数据需要通过qs编码才可以直接使用
if (config.data) {
config.data = qs.stringify(config.data)
}
}
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params);
url = url.slice(0, -1);
config.params = {};
config.url = url;
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200;
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false;
store.dispatch('LogOut').then(() => {
location.href = process.env.VUE_APP_CONTEXT_PATH + "index";
})
}).catch(() => {
isRelogin.show = false;
});
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
}
// else if (code === 500) {
// Message({ message: msg, type: 'error' })//redis错误,待解决
// return Promise.reject(new Error(msg))
// }
else if (code === 601) {
Message({ message: msg, type: 'warning' })
return Promise.reject('error')
} else if (code !== 200) {
Notification.error({ title: msg })
return Promise.reject('error')
} else {
return res.data
}
},
error => {
console.log('err' + error)
let { message } = error;
if (message == "Network Error") {
message = "后端接口连接异常";
} else if (message.includes("timeout")) {
message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常";
}
Message({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
// 通用下载方法
export function download(url, params, filename, config) {
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob',
...config
}).then(async (data) => {
const isBlob = blobValidate(data);
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text();
const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg);
}
downloadLoadingInstance.close();
}).catch((r) => {
console.error(r)
Message.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close();
})
}
export default service
版权归原作者 GISer_Jinger 所有, 如有侵权,请联系我们删除。