一、业务需求问题描述
- 简单场景: 按钮提交请求一个服务端接口,接口长时间未响应,按钮重复提交不允许再次请求服务端接口
- 复杂场景:上述场景只是一个很简单场景描述,有时候一个按钮提交处理逻辑有涉及页面许多表单输入控件的校验,还有处理dialog的显示(确定,取消逻辑)以及处理请求多个服务端接口的逻辑,按钮重复提交不允许再次请求服务端接口,甚至不允许点击页面中的其他按钮操作,这种场景一般在业务中经常遇到,那么该如何去正确处理按钮重复点击带来的问题
二、普通的解决方案
大部分人的做法都会先声明一个变量(如submiting),然后根据该boolean变量去决定是否去请求服务端接口,如下示例代码:
import axios from 'axios'
// 是否正在提交中
let submiting = false
/**
* 表单校验
* @return {Boolean}
*/
function validateForm() {
// TODO
}
/**
* 提交按钮逻辑监听器
* 注意:submitListener是一个async异步函数,返回值默认是一个promise,该promise状态必须是fulfilled或者rejected,而不是pending状态
*/
async function submitListener() {
if (submiting) {
return
}
submiting = true
if (!validateForm()) {
submiting = false
return
}
await axios.post('/api/xxx')
.then(res => {
// TODO
})
.catch(err => {
// TODO
})
await axios.post('/api/yyy')
.then(res => {
// TODO
})
.catch(err => {
// TODO
})
submiting = false
}
上述解决方案没有问题,缺点是如果一个页面有多个这样的逻辑,甚至一个项目有n处这样的逻辑需要处理,那么就需要我们去解放自己的生产力了
三、正确的解决方案
1 vue解决方案
1.1 提供vue的一个原型方法$lockListener
// lockListener.js
/* eslint-disable no-param-reassign */
/**
* lock 处理按钮重复点击
* @param {Function} handler 按钮click事件监听器,handler返回值一定需要是一个promise(且不能是pending状态)
* @param {Number} delay 按钮连续点击之间延时多少毫秒执行代码,handler代码中有异常(包含Promise.reject()抛出的异常)不生效
* @param {Function} lockValueChangeListener 按钮click事件监听器锁定值change事件监听器
*/
export default function lock(
handler,
delay = 1000,
lockValueChangeListener = () => {},
) {
// @思考1:为什么加一个lockValueChangeListener回调函数参数
let callback = () => {};
if (typeof lockValueChangeListener === 'function') {
callback = (bl) => lockValueChangeListener(bl);
}
return async (...args) => {
if (handler.$lock) {
return;
}
handler.$lock = true;
callback(true);
// @思考2:下面代码为什么不这样处理`await handler.apply(this, args).catch(err => Project.reject(err))`
// 还要赋值一个promise,然后promise.catch...
const promise = handler.apply(this, args);
promise.catch(() => {
handler.$lock = false;
callback(false);
});
await promise;
await new Promise((resolve) => setTimeout(resolve, delay));
handler.$lock = false;
callback(false);
};
}
// main.js app入口文件
import Vue from 'vue'
import lockListener from './lockListener.js'
Vue.prototype.$lockListener = lockListener
1.2 vue解决方案示例
备注:v-loading 是element-ui框架提供的指令
下面示例提交按钮连续点击之间的时间间隔最小是2秒,submitListener才会执行,如果submitListener执行耗时3秒,那么再次重复提交submitListener会立即执行
<template>
<div class="xxx-page">
<div
element-loading-text="提交中,请稍后..."
v-loading.fullscreen.lock="submitLocking"
@click="(() => $lockListener(submitListener, 2000, locking => submitLocking = locking))()"
>提交</div>
</div>
</template>
<script>
export default {
data() {
return {
// 提交锁定中(提交中)
submitLocking: false,
}
},
methods: {
async submitListener() {
if (!this.validateForm()) {
return Promise.reject()
}
await axios.post('/api/xxx')
.then(res => {
// TODO
})
.catch(err => {
// TODO
})
await axios.post('/api/yyy')
.then(res => {
// TODO
})
.catch(err => {
// TODO
})
}
}
}
</script>
1.3 上述代码留下的思考
- 为什么加一个lockValueChangeListener回调函数参数
主要是监听锁定值的变化,从vue解决方案示例中可以看到,我们可以根据submitLocking来处理外部逻辑,如:页面显示是否提交中loading;如果正在提交中页面其他是否可以被点击之类的逻辑
- 为什么不这样处理
await handler.apply(this, args).catch(err => Project.reject(err))
原因是如果代码有异常,浏览器控制台点击错误信息不能直观的找到真正出错的代码行,不好定位具体哪块代码出现错误
2 react解决方案
2.1、提供一个装饰器函数
// lock.js
/**
* @param {Number} delay 按钮连续点击之间延时多少毫秒执行代码,handler代码中有异常(包含Promise.reject()抛出的异常)不生效
* @param {Function} lockValueChangeListener 按钮click事件监听器锁定值change事件监听器
*/
export default function lock(
delay = 1000,
lockValueChangeListener = () => {},
) {
/**
* @param {Object} target 装饰目标(类的原型对象)
* @param {String} name 装饰目标成员(方法)
* @param {Object} descripter 类原型对象的方法属性描述符
*/
return function(target, name, descripter) {
const handler = descripter.value
let callback = () => {};
if (typeof lockValueChangeListener === 'function') {
callback = (bl) => lockValueChangeListener(bl);
}
descripter.value = async function() {
if (handler.$lock) {
return
}
handler.$lock = true
callback(true)
const promise = handler.apply(this, arguments)
promise.catch(() => {
handler.$lock = false
callback(false)
})
await promise
await new Promise(resolve => setTimeout(resolve, delay))
handler.$lock = false
callback(false)
}
return descripter
}
}
2.2、react解决方案示例
import React from 'react'
import lock from './lock'
class Page extends React.Component {
@lock(2000)
async submitListener() {
if (!this.validateForm()) {
return Promise.reject()
}
await axios.post('/api/xxx')
.then(res => {
// TODO
})
.catch(err => {
// TODO
})
await axios.post('/api/yyy')
.then(res => {
// TODO
})
.catch(err => {
// TODO
})
}
render() {
return (
<div class="xxx-page">
<div onClick={ this.submitListener.bind(this) }>提交</div>
</div>
)
}
}