前端代码连续点击多次按钮,导致数据提交多次,导致后端幂等,解决方案(全局 每次前端请求设置Loading加载效果,请求完成后,关闭Loading加载)

本文探讨了编程中的幂等概念,特别是在前端开发中的应用。通过介绍如何处理多次请求以避免幂等问题,文章详细讲解了如何使用Element UI的loading效果,以及结合axios的请求和响应拦截器实现全局loading加载。在处理多个请求时,文章提出了改进的loading效果,确保所有请求完成后再关闭loading,从而提高用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、文章开篇:什么是幂等?

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。

简单说:在编程中就是发多次请求,但是请求都是一样的,结果是一样的。

比如说,向数据库中新增1条数据,其实简单的来说就是设一个点击按钮,这个都没有问题,但是假如说我们向后端传输的数据有很多,页面并没有说及时得到反映,这个时候我们应该如何去处理?

其实很多前端程序员的通病就是写代码的时候不考虑代码的复用以及组件的扩展性,包括我个人也是一样,这样就会导致后期要维护的时候Bug有很多,同样代码的耦合性也会同样很高,当然这样肯定是不好的。

好的言归正传说会 幂等这个问题,如果是增加一条数据,那么前端页面肯定是会去触发一个事件,但是如果我们手抖不小心点击了两次呢?是不是就会向后端请求两次数据呢?

答案当然会,其实这就是幂等问题,包括像删除,假如说我要根据一条数据的id去删除数据,我第一次点击按钮请求时,那么数据库后端已经根据这个id去对数据库中的数据进行了操作,但是如果我在请求的过程中又点击了一次,其实这个时候如果后端再去根据这个id去更改变更数据库。仔细考虑一下,会出现什么问题呢?

当然言归正传,后端自然会有后端的处理方案,悲观锁,乐观锁,本地锁等等。

当然今天我们主要讲的就是前端如何去处理,尽量去减少后端的幂等?

1、前端怎么样去做处理?

在这里插入图片描述
看到这个转圈圈是不是很熟悉?
没错就是新增一个loading效果,每次当我们向后端发送请求时,新增一个转圈圈效果那不就好了么?

在这里的话我们就不讲js原生了,因为其实loading的原理就是创建一个隐藏的遮罩层,当用户点击button的时候,就把这个遮罩层展示出来,当数据已经从服务器返还回来时,就让这个遮罩层隐藏。

当然在现在基本上我们都是用ui框架的年代里,loading肯定是各个ui库都是已经给我们封装好了的,那么我们今天就来详细的介绍一下关于element的loading加载效果

2、效果

首先我们可以看一下官网给我们的案例

<template>
  <el-button
    type="primary"
    @click="openFullScreen1"
    v-loading.fullscreen.lock="fullscreenLoading">
    指令方式
  </el-button>
  <el-button
    type="primary"
    @click="openFullScreen2">
    服务方式
  </el-button>
</template>

<script>
  export default {
    data() {
      return {
        fullscreenLoading: false
      }
    },
    methods: {
      openFullScreen1() {
      // 当点击了指令方式时就会触发 loading加载效果  指令方式是使用v-loading方式指令这种形式
        this.fullscreenLoading = true;
        
        setTimeout(() => {
        // 2s之后关闭loading加载效果
          this.fullscreenLoading = false;
        }, 2000);
      },
      openFullScreen2() {
      // 当点击了 服务方式时  首先触发一个加载效果,loading中的参数对象就是配置项,让其显示何种样式的遮罩层
        const loading = this.$loading({
          lock: true,
          text: 'Loading',
          spinner: 'el-icon-loading',
          background: 'rgba(0, 0, 0, 0.7)'
        });
        setTimeout(() => {
        // 2s后关闭对应的遮罩层。
          loading.close();
        }, 2000);
      }
    }
  }
</script>

在这里插入图片描述
从逻辑上来说,我们前端每次在点击按钮之前,就设置上遮罩,当数据返还回来时就直接给这个遮罩层关了.

但是我们需要考虑的问题就是,如果我们这样去做,请注意,我们给每一个请求去添加loading效果,首先本身这就是一件非常费力的事情,当然如果产品有需求,要求严格指定什么需要添加loading效果的话,那么就必须去单独的去加了,不过最好还是封装一个全局的方法吗,然后去调用一下,这样肯定是比较好的,这个时候我们就可以在app.vue里使用provide、inject 这种方式来传递this调用这个父根方法,当然也可以main入口文件定义在prototype上,挂载在vue的原型对象上,到时候用的时候直接去调用即可.

3、配合axios,请求拦截器,相应拦截器使用loading效果。

在需求没有那么严格的情况下,首先我们肯定是有简单,高效的方法来解决多次点击按钮这个问题的,那么既然我们使用的是vue框架的话(当然其他框架像react也是同样一个道理,配置axios即可),我们就可以直接在axios的请求拦截器、响应拦截器中实现loading这个加载的效果。

首先我们来看axios的官网给出我们拦截器的api

在这里插入图片描述
拦截器
在请求或响应被 then 或 catch 处理前拦截它们。


// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

我们就可以根据拦截器来做一系列的事件

直接上代码

import axios from 'axios'
import router from '@/router'
import qs from 'qs'
import { Loading } from 'element-ui'; // 记住按需导入
// 因为我在这边是使用的自定义的axios 性质是一样的,定义了一个全局变量,根据此全局变量来跟进
const service = axios.create({
  baseURL: '',
  timeout: 30000,
  withCredentials: true
})
// 请求拦截器
let loadingInstance = ''
service.interceptors.request.use(config => {
  // 增添token
  config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  loadingInstance = Loading.service({
    lock: true,
    text: 'Loading',
    spinner: 'el-icon-loading',
    background: 'rgba(0, 0, 0, 0.7)'
  });
  return config
}, err => {
  Promise.reject(err)
})
// 响应拦截器
service.interceptors.response.use(response => {
  // 以服务的方式调用的 Loading 需要异步关闭
  loadingInstance.close();

  // 如果正常直接返回对应data请求数据
  return response.data
}, err => {
  return Promise.reject(err)
})

此样即实现了全局效果,但是如果说真的就一点问题都没有了吗?
那肯定不是的,注意,一旦当我们返还了数据之后对应的loading加载效果就已经清空,但是如果一个页面加载的时候发送传递了多条请求呢?项目中一个页面是不可能只发送一个请求的。

我们上方,但凡只要有数据返还那么就会去除loading效果,那如果其他的数据没有返还回来呢?loading是不是还应该在一个加载的过程中?

4、改写、完善Loading加载效果

// 请求拦截器
let requestNum = 0
let loadingInstance = ''
service.interceptors.request.use(config => {
  // 每次请求时requestNum++
  requestNum++
  config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
  loadingInstance = Loading.service({
    lock: true,
    text: 'Loading',
    spinner: 'el-icon-loading',
    background: 'rgba(0, 0, 0, 0.7)'
  });

  return config
}, err => {
  // 如果请求一旦出现问题 requestNum = 0 loadingInstance.close();
  requestNum = 0
  loadingInstance.close();
  Promise.reject(err)
})
// 响应拦截器
service.interceptors.response.use(response => {
  // 每次请求时-- 主要时用于判断当前共请求了多少次,如果返还的次数不等于请求的次数的话就不会进入这个条件
  // 只有当所有数据全返还时,才会关闭loading效果
  requestNum--
  if (requestNum <= 0) {
    loadingInstance.close();
    requestNum = 0
  }
  // 如果正常直接返回对应data请求数据
  return response.data
}, err => {
  // 如果请求一旦出现问题 requestNum = 0 loadingInstance.close();
  requestNum = 0
  loadingInstance.close();
  return Promise.reject(err)
})

这样当所有页面全部返还之后才会将loading加载效果关闭

<think>好的,我现在需要解决用户的问题:在UniApp中如何防止按钮重复点击的最佳实践。首先,我得回忆一下UniApp的相关知识,尤其是处理用户交互和前端逻辑的部分。 用户提到他们参考的站内引用中有关于幂等性最佳实践的内容,特别是使用Token和Redis来保证操作的唯一性。不过,UniApp作为前端框架,可能更关注前端层面的解决方案,但如果有需要后端配合的情况,可能也需要考虑。 首先,前端防止重复点击的常见方法包括按钮防抖(debounce)、禁用按钮状态、或者使用标志位来控制点击事件。在UniApp中,通常使用Vue.js的语法,所以可以考虑利用Vue数据绑定特性来控制按钮的状态。 比如,当用户点击按钮后,立即将按钮设置为禁用状态,防止再次点击。等待操作完成后再恢复按钮状态。这种方法简单有效,但需要确保在异步操作完成后正确恢复状态,否则按钮可能一直禁用。 另外,防抖函数也是一个常用的方法,可以限制函数在一定时间内只执行一次。在UniApp中,可以封装一个防抖函数,或者在点击事件处理函数中加入防抖逻辑。 不过,用户提到的最佳实践中,引用[3]提到使用全局ID(Token)和后端校验,这可能涉及到前后端协作的幂等性处理。如果用户的场景是提交表单或支付等关键操作,仅前端控制可能不够,需要后端配合确保请求的唯一性,比如通过Token机制。 所以,可能需要分两种情况:前端自身的防重复点击,以及需要后端支持的幂等性处理。用户的问题可能更侧重于前端实现,但最佳实践可能建议结合两者。 接下来,我需要考虑在UniApp中具体的实现方式。例如,使用Vue的data属性来控制按钮是否可点击,或者在methods中使用防抖函数。同时,还要注意用户体验,比如禁用按钮时显示加载状态,避免用户困惑。 另外,UniApp支持使用自定义指令,可能可以创建一个防重点击的指令,简化在多个按钮上的应用。不过,这可能对初学者来说有点复杂,所以简单的标志位方法可能更易实现。 还需要考虑异步操作的情况,比如发起网络请求后,在回调函数中恢复按钮状态。如果使用async/await,可能需要用try/finally来确保按钮状态恢复,即使请求失败也不会让按钮一直禁用。 可能的问题点包括:如何处理多个按钮的情况?是否需要为每个按钮单独维护状态?或者使用全局状态管理?不过对于简单应用,每个按钮自己管理状态可能更直接。 综上所述,最佳实践可能包括以下几个步骤: 1. 在按钮点击时,立即禁用按钮,防止二次点击。 2. 显示加载状态,例如显示“提交中...”或加载图标。 3. 在异步操作(如API请求完成后,无论成功与否,恢复按钮状态。 4. 对于关键操作,结合后端幂等性校验,如使用Token机制。 然后,我需要将这些思路转化为具体的代码示例,并确保符合UniApp的语法和最佳实践。例如,使用Vue的data属性中的isLoading变量控制按钮的disabled属性和显示文本。 同时,可能需要处理网络请求较慢的情况,避免用户等待时重复点击,因此禁用按钮和视觉反馈都很重要。 最后,验证这些方法是否有效,例如测试快速多次点击按钮是否只触发一次请求,并且在请求结束后按钮恢复可用状态。</think>### 如何在 UniApp 中防止按钮重复点击的最佳实践 在 UniApp 中防止按钮重复点击的核心思路是通过 **前端状态控制** 结合 **后端幂等性设计** 实现。以下是具体实现方案: --- #### **1. 前端状态控制** 通过禁用按钮或添加加载状态,从交互层面限制用户重复操作。 ##### **实现步骤**: 1. **绑定按钮禁用状态**:使用 `data` 变量控制按钮的 `disabled` 属性。 2. **添加加载动画**:提升用户体验,明确反馈操作状态。 3. **异步操作完成后恢复状态**:无论成功或失败,最终恢复按钮点击性。 ##### **示例代码**: ```html <template> <button :disabled="isLoading" @click="handleClick"> {{ isLoading ? '提交中...' : '提交' }} </button> </template> <script> export default { data() { return { isLoading: false }; }, methods: { async handleClick() { if (this.isLoading) return; // 防止重复触发 this.isLoading = true; try { await this.submitData(); // 模拟异步请求 uni.showToast({ title: '提交成功' }); } catch (error) { uni.showToast({ title: '提交失败', icon: 'none' }); } finally { this.isLoading = false; // 恢复按钮状态 } }, submitData() { return new Promise((resolve) => { setTimeout(resolve, 2000); // 模拟接口请求 }); } } }; </script> ``` --- #### **2. 防抖函数封装** 通过函数防抖(Debounce)限制高频点击,确保一定时间内只触发一次。 ##### **工具函数**: ```javascript function debounce(fn, delay = 1000) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; } ``` ##### **使用示例**: ```html <template> <button @click="debouncedClick">防抖按钮</button> </template> <script> export default { methods: { debouncedClick: debounce(function() { console.log('仅触发一次'); }, 1000) } }; </script> ``` --- #### **3. 后端幂等性设计(关键操作必选)** 对于支付、订单提交等场景,需结合后端验证全局唯一请求 ID,避免网络重试导致重复执行[^3]。 ##### **实现流程**: 1. **前端请求 Token**:在用户操作前,向后端申请全局唯一 Token。 2. **携带 Token 提交请求**:将 Token 放入请求头或参数中。 3. **后端校验 Token**:通过 Redis 校验 Token 有效性,执行后立即删除 Token。 ##### **前端示例**: ```javascript async submitOrder() { const token = await this.getToken(); // 获取唯一 Token const res = await uni.request({ url: '/api/submit', header: { 'X-Idempotent-Token': token } }); // 处理响应 } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

归来巨星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值