各位大佬,前端按钮重复点击提交请求的最佳方法? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
LyleRockkk
V2EX    Vue.js

各位大佬,前端按钮重复点击提交请求的最佳方法?

  •  3
     
  •   LyleRockkk 2019-09-05 10:18:12 +08:00 17107 次点击
    这是一个创建于 2310 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前我知道的有 3 种 1:按钮绑定一个变量绑定 disable,请求完成后改为 false (每个按钮都要加感觉有点蠢) 2:ajax 之前 全局加 loading 弹层, 防止重复点击(如果时间很短,loading 也影响体验) 3:Vue 中 弄个指令,给一个时间,该时间内只执行一次事件(这个时间感觉也不靠谱,太长太短都不好)

    77 条回复    2019-09-09 16:21:13 +08:00
    alexmy
        1
    alexmy  
       2019-09-05 10:23:37 +08:00   3
    lodash 库的 throttle, debounce,我的个人小站就用这个。
    dmjob2015222
        2
    dmjob2015222  
       2019-09-05 10:25:18 +08:00
    防抖、节流
    flyingfz
        3
    flyingfz  
       2019-09-05 10:26:17 +08:00   6
    http 接口 的幂等性的要求 , 了解一下 。
    GzhiYi
        4
    GzhiYi  
       2019-09-05 10:26:38 +08:00
    节流一把梭
    otakustay
        5
    otakustay  
       2019-09-05 10:29:39 +08:00
    你可以考虑做个 Button,onClick 允许返回 Promise 自己处理 disabled
    LyleRockkk
        6
    LyleRockkk  
    OP
      nbsp;2019-09-05 10:30:11 +08:00
    @alexmy 刚刚看了一下这两个 func, 都是要给一个 wait 时间的,跟我说的方法 3 一个道理,我意思就是这个时间不太好给,太长太短都不行,最好能对于接口响应的时间
    Variazioni
        7
    Variazioni  
       2019-09-05 10:34:32 +08:00   1
    只有幂等的接口才能完美解决这个问题。。。
    hlwjia
        8
    hlwjia  
    PRO
       2019-09-05 10:34:35 +08:00 via iPhone
    2 就可以,体验那里你多虑了。

    除非你是亚马逊那种页面加载慢一点都损失几个 million 收入的公司
    roscoecheung1993
        9
    roscoecheung1993  
       2019-09-05 10:39:24 +08:00
    方案一可以的,按钮做个统一封装就好了,毕竟也不是所有按钮都需要加防重提交是吧
    murmur
        10
    murmur  
       2019-09-05 10:42:51 +08:00
    disable 是一定要加的
    learnshare
        11
    learnshare  
       2019-09-05 10:43:03 +08:00   2
    “每个按钮都要加感觉有点蠢” 写业务逻辑不就是这样么
    方案 2 3 更蠢好么
    LyleRockkk
        12
    LyleRockkk  
    OP
       2019-09-05 10:45:57 +08:00
    @learnshare 先把需要的按钮单独处理再说,之后再研究全局的方式,哈哈
    cmdOptionKana
        13
    cmdOptionKana  
       2019-09-05 10:49:54 +08:00
    加 disable 的好处是,可以有视觉效果让用户明白正在处理中。否则用户不知道究竟点上没有,会烦躁地多点几下。
    lihongjie0209
        14
    lihongjie0209  
       2019-09-05 10:51:11 +08:00
    @flyingfz #3 说的简单, 前端一个 disable 的事情后端复杂的要死
    Augi
        15
    Augi  
       2019-09-05 10:53:09 +08:00
    我觉得 可以用方案一,disable 可以以 loading 的形式呈现,现在好多成熟的组件库也都有 按钮的 loading 状态,可以统一封装下。
    Augi
        16
    Augi  
       2019-09-05 10:54:18 +08:00
    @alexmy 这两个函数我光看名字每次都傻傻分不清楚。中文名一个去抖,一个节流,然后就更晕了。
    Greesea
        17
    Greesea  
       2019-09-05 10:56:20 +08:00
    表单一次性 token
    zppass
        18
    zppass  
       2019-09-05 11:04:42 +08:00
    写个组件吧,用的时候调用一下就可以。
    有的云服务,提交 API 请求会有一个时间设置,防止时间间隔内重复提交,阿里云那个在 API 选项,放置重复攻击。
    ChefIsAwesome
        19
    ChefIsAwesome  
       2019-09-05 11:10:29 +08:00 via Android
    1.用户按回车照样提交。
    2.提交成功了或者失败了才有可能让用户再次提交,不是什么 debounce 之类的。

    要 disable 也是 disable 那个 form。你那 123 都不靠谱。任何请求都是这三个状态:pending,resolved,reject。遮罩或者按钮怎么显示,是这个 form 不同状态下的不同 ui。
    acthtml
        20
    acthtml  
       2019-09-05 11:12:24 +08:00
    方案 1 最佳
    LiuJiang
        21
    LiuJiang  
       2019-09-05 11:13:00 +08:00
    @alexmy 为啥不自己些呢,lodash 这么大的库
    jowan
        22
    jowan  
       2019-09-05 11:14:52 +08:00
    你没发现很多应用 点击按钮后 按钮就变成半透明或者灰色不可点击状态 并且变成 loading 了吗
    其实就 1 方案的改良版
    不管你用什么方法 你封装成组件就行了 1 2 3 都行
    可以参考 vue 的各大框架的按钮 基本都有个 loading 属性
    LyleRockkk
        23
    LyleRockkk  
    OP
       2019-09-05 11:17:01 +08:00
    @ChefIsAwesome 没用 form 表单形式提交,直接 function 封装 ajax 走的后台接口
    arslion
        24
    arslion  
       2019-09-05 11:24:17 +08:00   1
    通过使用加 disabled 等控制界面的方式来保障交互体验
    但逻辑才是最必要的,在前端使用 debounce 和 loading flag,在后端实现幂等接口
    toma77
        25
    toma77  
       2019-09-05 11:25:14 +08:00
    方法 2
    Woodywuuu
        26
    Woodywuuu  
       2019-09-05 11:26:58 +08:00
    前后端都得搞。
    提个前端思路,用 XHR 的 abort 方法。在路由库里面做判断,可以选择性中断不想要的请求。
    想了下应用场景:
    1. 切换路由时将前一个请求 abort。
    2. body 相同的 post 请求,在前一个还在执行的时候,不允许后续提交。
    3. ....暂时没想到
    目前线上用的是 axios 实现的,效果还行。
    abel1989
        27
    abel1989  
       2019-09-05 11:30:14 +08:00
    自己用 VUE 封装一个 BUTTON 的组件
    Vegetable
    &bsp;   28
    Vegetable  
       2019-09-05 11:38:55 +08:00
    不封装通用功能才是蠢哦
    Raymon111111
        29
    Raymon111111  
       2019-09-05 11:49:44 +08:00
    这种按钮组件不应该是封装好的吗?

    前端做了之后能拦住大部分

    然后后端再把那种恶意发请求的拦住就行了
    xrr2016
        30
    xrr2016  
       2019-09-05 11:56:00 +08:00 via Android
    让后端加 redis 缓存啊,每个请求一个 key,有重复就报错
    dartabe
        31
    dartabe  
       2019-09-05 11:57:26 +08:00
    vue 和 react 都能封装各种按钮吧
    molvqingtai
        32
    molvqingtai  
       2019-09-05 12:00:38 +08:00 via Android
    @LiuJiang lodash 支持按需加载啊
    zhuweiyou
        33
    zhuweiyou  
       2019-09-05 12:03:44 +08:00
    用 disable 的方式。
    评论说 防抖、节流的,这只能控制点击的频率 /频次。

    经常会有这种场景,比如: 点击支付。
    你搞个防抖是不对的,因为我这订单就只能付一次,我多点几下就不对了。
    ChefIsAwesome
        34
    ChefIsAwesome  
       2019-09-05 12:27:05 +08:00
    @LyleRockkk 那就是这个 button 得有那三个状态,根据不同状态来显示不同的东西。而且 button 光显示遮罩也是不行的。用户点了一次之后,按到空格照样会造成点击。你必须得根据请求的状态,html 里 disable 或者在 onClick 里头 return 掉。如果你是个比较复杂的大程序,把 api 请求这层分出来了,为了保险起见,也应该在 api 请求那里设置 flag,请求没完成之前不能再发请求。
    keelii
        35
    keelii  
       2019-09-05 12:51:57 +08:00
    jQuery 中有个封装事件叫做 one 实际上就是事件处罚完了立即解绑。完事了你可以按需求再绑回去。
    BOYPT
        36
    BOYPT  
       2019-09-05 12:55:39 +08:00
    没 vue 经验,但在 angular 里面我用 ng-disable 绑定一个 ajax 过程的配置的 scope 变量,也就是相当于方法 1 吧,ajax 过程我做了封装,所有按钮只需要设置那一个参数。
    kisshere
        37
    kisshere  
       2019-09-05 13:14:56 +08:00
    个人倾向于 disable
    subpo
        38
    subpo  
       2019-09-05 13:16:04 +08:00
    @flyingfz #3 post 不可能幂等
    subpo
        39
    subpo  
       2019-09-05 13:17:15 +08:00
    @flyingfz #3 搜了一下,看来是我理解的不对,忽略我
    qiaobeier
        40
    qiaobeier  
       2019-09-05 13:34:58 +08:00
    所谓异步编程。ajax 的几个阶段都做成全局的 events 算了。
    simonv3ex
        41
    simonv3ex  
       2019-09-05 13:55:36 +08:00
    你让前端的所有请求都走一个通道,disable 的开关就在那控制,再封一个 Buttom 或其他数据相关的组件,里面的 disable 就连这个通道的 disable
    bhaltair
        42
    bhaltair  
       2019-09-05 14:08:18 +08:00
    1 点击 debounce
    2 axja 拦截重复请求

    目前做到了 1
    flashback313
        43
    flashback313  
       2019-09-05 14:10:01 +08:00
    disable 其实并没有什么问题,另外可以尝试全局 loading 就是蒙层那种,一旦发出请求就弹出
    abelmakihara
        44
    abelmakihara  
       2019-09-05 15:04:32 +08:00
    这几种我还是喜欢用 loading 其次 lodash 的
    s247769541
        45
    s247769541  
       2019-09-05 15:10:18 +08:00
    封装组件,用一个 disabled 属性控制下面所有表单元素的 disabled 属性。。。 参考 element-ui 的
    px920906
        46
    px920906  
       2019-09-05 15:32:44 +08:00
    按钮 disable 掉+loading 动画就挺好。
    原生 js 或者 jquery,可以封装在 ajax 库里,比如叫 btnGet,把按钮元素作为参数传进去
    vue 的话,在 data 里加一个 loading 对象,属性初始值都为 false,发起请求前 loading.ajaxCall1 = true, 成功或失败后 loading.ajaxCall = false。
    另外,axios 有个 cancel 的功能 -> https://github.com/axios/axios#cancellation
    用这个给项目加了取消重复请求的功能 ,目前看来还不错。
    quanjw
        47
    quanjw  
       2019-09-05 15:47:41 +08:00
    $("#submit").attr({"disabled":"disabled"});
    $("#submit").removeAttr("disabled");
    walhu
        48
    walhu  
       2019-09-05 15:54:37 +08:00
    加一个验证码机制。每次访问之后后段刷新。这样就不怕了
    Melting
        49
    Melting  
       2019-09-05 16:00:41 +08:00
    之前为了偷懒,给请求做一个 lrucache,短时间的非 get 又是同样的请求,可以取消,用 axios.cancelToken 很好实现
    g0thic
        50
    g0thic  
       2019-09-05 16:02:43 +08:00
    2 就可以了,如果你不喜欢页面 loading,就封装个按钮组件,按钮加 loading 状态
    jkmf
        51
    jkmf  
       2019-09-05 16:09:26 +08:00
    @zhuweiyou 请求回来之前不允许点击 怎么提交多次呀老哥
    xianxiaobo
        52
    xianxiaobo  
       2019-09-05 16:21:43 +08:00
    v 站很多时候我好像只点击了一次,但是说我回复和上一次相同,就给我取消了.
    jevirs
        53
    jevirs  
       2019-09-05 16:27:07 +08:00
    我有个想法: 利用 request 和 response 的拦截器,每一个接口对应一个状态,request 出去的时候在全局的 map 中将对应的接口状态改为 pending,新的 request 进来,如果是 pengding 状态就直接返回了;
    response 回来之后,再把对应的状态改为 done,这样就可以就收新的 request 出去;
    不确定 reponse 拦截器里是否能找到对应的 request...
    chen2019
        54
    chen2019  
       2019-09-05 16:30:13 +08:00
    组成 [md5(data)]=有效时间 然后请求前判断 key 值为 md5(data)是否存在 结合有效时间 决定是否需要阻止这个请求就可以了
    yc8332
        55
    yc8332  
       2019-09-05 16:35:54 +08:00
    前端应该就是加个变量锁住吧。。后端也是要锁住,redis increment 可以解决
    zhixuanziben
        56
    zhixuanziben  
       2019-09-05 16:48:12 +08:00
    @LiuJiang lodash 支持按需加载,只引自己想要的就行了
    winiex
        57
    winiex  
       2019-09-05 16:54:18 +08:00
    理解 debounce 的流程就好。如果不想引入库的话自己用 setTimeout 写一个也很简单。
    lizz666
        58
    lizz666  
       2019-09-05 17:36:35 +08:00
    节流,最好能自己写
    zhazi
        59
    zhazi  
       2019-09-05 17:51:49 +08:00
    etag
    zongwan
        60
    zongwan  
       2019-09-05 19:37:44 +08:00
    不就是验证码吗。。。
    TomVista
        61
    TomVista  
       2019-09-05 19:55:21 +08:00 via iPhone
    我用的是异步函数自我同步。
    用闭包保留一个状态,isDoing-true,回调或者 await 之后,改 isDoing-false,
    这样,在执行队列中只能有一个该函数
    RubyJack
        62
    RubyJack  
       2019-09-05 20:38:52 +08:00
    幂等接口真是笑死我了,button 引入点击状态是真的难, 只能甩锅给后端了

    加购物车这种场景, 谁来示范一下幂等?
    redbuck
        63
    redbuck  
       2019-09-05 21:36:37 +08:00
    方案一改一改。

    接口有统一封装就改请求函数,没有就劫持 ajax。
    用链接和参数做 key,请求就标记,回来就干掉。
    lihongjie0209
        64
    lihongjie0209  
       2019-09-05 21:40:14 +08:00
    @RubyJack #62 有些人认为接口幂等很简单, 没办法
    jss
        65
    jss  
       2019-09-05 22:01:47 +08:00 via iPhone
    就不能给按钮一个 loading ?
    4DAX07B8Kle4Dm6T
        66
    4DAX07B8Kle4Dm6T  
       2019-09-05 22:15:14 +08:00 via iPhone
    表单令牌了解一下
    wupher
        67
    wupher  
       2019-09-06 09:25:26 +08:00
    抛开前端不提,后端也要防重放攻击啊。

    人家不通过前端,直接跑个脚本发 http 请求攻击肿么办?
    lihongjie0209
        68
    lihongjie0209  
       2019-09-06 09:52:01 +08:00
    @wupher #67 攻击==表单重复提交??
    wupher
        69
    wupher  
       2019-09-06 10:11:53 +08:00
    @lihongjie0209 重放攻击是这样。如果你某个业务需要特别多的计算,而且结果无法实现幂等,那么攻击者可以通过录制请求或者伪造请求。大量发往服务器,实现攻击目的。
    lihongjie0209
        70
    lihongjie0209  
       2019-09-06 10:28:03 +08:00
    @wupher #69
    用户提交表单的时候我们默认用户处于一个安全的状态, 比如登录,验证码, 表单签名。

    攻击者处于我们系统的最外围, 要攻击必须先突破我们的安全限制才能进行下一步的动作。


    你把表单重复提交这种安全域范围内的事情当作攻击, 就相当于你为了预防 1000KM 外的狙击手每天呆在地下堡垒
    wupher
        71
    wupher  
       2019-09-06 11:17:36 +08:00
    @lihongjie0209 这个看业务,看团队,看技术了。我相信表单和表单也不一样,对吧?

    个人觉得,像支付宝付款那个表单,怎么防范都是应该的,对吧。

    防重放我觉得其实也没你想的那么难,有很多简单的策略和实现就能初步实现。实现后通过诸如 AOP、API Gateway、Filter 都是简单配置一下即可。并不会对业务开发造成太高代价。
    lihongjie0209
        72
    lihongjie0209  
       2019-09-06 11:32:18 +08:00
    @wupher #71
    重放不难防范, 但这个不是我们是使用一项技术的原因。一大堆简单的技术放在一起复杂度还是会大量的增加
    lihongjie0209
        73
    lihongjie0209  
       2019-09-06 11:33:46 +08:00
    @wupher #71 至于你说的某些特别重要的表单, 那么应该是针对几个表单的接口做安全处理, 而不是全局性的
    RV0n
        74
    RV0n  
       2019-09-06 13:57:01 +08:00
    加锁
    mazai
        75
    mazai  
       2019-09-06 14:30:37 +08:00
    加个 loading 防护罩
    source
        76
    source  
       2019-09-09 16:19:54 +08:00
    前端这边可以写个工具方法,需要防重复提交的 api 用它包装一下(假如它返回一个 promise )
    // mock api
    function api(params) {
    return new Promise(function(resolve, reject) {
    setTimeout(() => resolve(params), 5000)
    });
    }

    // 生成带锁的 api
    function lockApi(api) {
    let lock = false
    return (...params) => {
    if (!lock) {
    lock = true
    return api(...params)
    .then(data => {
    lock = false
    return data
    })
    } else {
    return Promise.reject('too frequent!')
    }
    }
    }
    source
        77
    source  
       2019-09-09 16:21:13 +08:00
    缩进炸了,贴张截图吧
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2360 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 42ms UTC 09:20 PVG 17:20 LAX 01:20 JFK 04:20
    Do have faith in wat you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86