前端防止用户同一时间多次发出同一个请求

在使用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值