防止按钮重复点击
按钮是前端界面中承接着用户操作的很重要的一个环节,前端界面用户和系统的交互都通过按钮来完成,与系统的交互自然就少不了把用户的意向保存到系统中,如今面对前后端分离的部署方案,前端与后端的通信都是通过接口来完成的。那么问题就来了发送一个接口就需要等待,那么等待的这段时间可长可短(根据用户当前的网络时间决定的),如果一个请求三秒以后才回来,用户在这一段时间再次点击怎么办。在如今这个网速很快的时代,可能延迟是非常低的,所以给用户考虑的时间并不多,但是如果请求时间长重复点击就需要做限制了。本次就是基于项目中的发送请求的按钮做防止重复点击的一些探索。、
- 现在的前端的项目中发送请求大都采用 async/await 的语法糖吧异步请求封装,正是因为是异步请求,回调之前的按钮都是不应该点击的,这样可以防止一些请求二次发送造成的一些bug。
- 举一个简单的例子,在项目中有一个按钮是提交按钮,把用户的的一些信息通过调用接口的形式发送给后端,然后跳转到详情页面,这就是一个很简单的前端交互的场景。我们来简单实现一下(基于React实现方式)
const ButtonClick: React.FC = () => {
const [value, setValue] = useState('');
const copyValue = useRef('');
const sendValue = (name: string): Promise<string> =>
new Promise((resolve: Function, reject: Function) => {
setTimeout(() => {
if (!name) {
return reject('请输入对应对容')
}
copyValue.current = name;
resolve('success');
}, 3000)
})
const getValue = async () => {
try {
const flag = await sendValue(value);
console.log(flag);
console.log(copyValue.current);
console.log('其他业务操作')
} catch (error) {
message.warning(error)
console.log(error)
}
}
const inputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value)
}
return (
<>
<Input onChange={inputChange} value={value} style = {{width: "300px"}}></Input>
<Button onClick={getValue} type = "primary" style = {{marginTop: "20px"}}>发送</Button>
</>
)
}
- 上面的代码就是简单把用户输入的内容给持久化起来,这里借用了setTimeout来模仿网络延迟的效果,如果在请求处理过程中不对重复点击进行控制,那么就会出现下面的情况,连续发送好几次请求,最终效果差强人意。
- 所以针对这个按钮就需要做重复点击控制,每次请求成功的时候才能恢复按钮的可点击状态。那么简单的实现代码就来了针对按钮点击的方法:
const [affectLoading,setAffectLoading] = useState(false);
const getValue = async () => {
if(affectLoading) {
return;
}
setAffectLoading(true);
try {
const flag = await sendValue(value).finally(() =>{
setAffectLoading(false)
})
console.log(flag);
console.log(copyValue.current);
console.log('其他业务操作')
} catch (error) {
message.warning(error)
console.log(error)
}
}
- 上面的方式针对按钮事件单独定义了一个变量进行了控制,每次都是请求完毕的时候把控制变量给置为false,上面这种方式确实是可行的但是如果针对每一个按钮事件都需要单独定义一个变量,会造成内部的变量很多后期很难维护。那么就像节流一样能不能针对这种情况抽离出一个公共的方法来实现呢。
- 经过梳理,如果我们抽离出一个方法,和这种单独的写法是一致的,首先定义一个变量置为false,然后进行第一次请求,这个时候给变量置为true,等待请求结束再给变量置为false,这样就达到了控制重复点击的效果。前几天解除了js的装饰器,首先想到的就是使用装饰器在代码编译的时候给注入这一过程。可以对于React Hook 是没有类这一个概念的。所以对于装饰器也用不了(但是对于React的class组件还是可以使用的。下面会给出实现方式)。对于React Hook则可以使用高阶函数的方式实现,传入一个方法,返回包装过的方法,高阶函数类似与下面的方式:
const demo = () => {
console.log('处理业务逻辑');
}
const warpButton = (buttonEvent:Function) => {
return () => {
console.log('begin');
buttonEvent();
console.log('end');
}
}
HTML:
<Button onClick={warpButton(demo)}>发送</Button>
- 经过warpButton的包装可以给注入的方法执行一个额外的逻辑,那么我们实现的逻辑也就可以基于次来实现了。下面是代码:
const getValue = async () => {
try {
const flag = await sendValue(value)
console.log(flag);
console.log(copyValue.current);
console.log('其他业务操作')
} catch (error) {
message.warning(error)
console.log(error)
}
}
const wrapButton = (buttonEvent: Function, messageValue?: string) => {
let flag = false;
return async function () {
if (flag) {
messageValue && message.warning(messageValue);
return;
}
flag = true;
//@ts-ignore
await buttonEvent.apply(this, arguments).finally(() => {
flag = false;
})
}
}
HTML:
<Button onClick={wrapButton(getValue,'loading')} type="primary" style={{ marginTop: "20px" }}>发送</Button>
- 通过这个高阶函数可以自动帮助我们在执行请求的时候控制对应的请求状态,这样就能够做到自动对我们注入的函数进行控制。同时可以根据传入的提示信息进行提示。
- 对于公共方法还需要在考虑一下兼容性,如果这里传入的就是一个普通的js方法这样就报错了,所以需要对传入的方法进行判断,增加兼容性:
代码如下
const wrapButton = (buttonEvent: Function, messageValue?: string) => {
let flag = false;
return async function () {
if (flag) {
messageValue && message.warning(messageValue);
return;
}
flag = true;
if (buttonEvent.constructor.name === 'AsyncFunction') {
//@ts-ignore
await buttonEvent.apply(this, arguments).finally(() => {
flag = false
})
} else {
//@ts-ignore
buttonEvent.apply(this, arguments);
flag = false;
}
}
}
- 对与React Hook 中可以使用高阶函数的方式可以实现,对于之前的class 组件则是可以使用装饰器了,不仅看上去美观同时使用起来也是比较方便。但是装饰器只能用于类和类的属性上,不能用于方法上,因为存在函数提升。直接给出装饰器代码:
const lockButton = (value: string = 'loading') => {
return (target?: any, key?: any, desc?: any) => {
const fn = desc.value;
let flag = false;
desc.value = async function () {
if (flag) {
message.warning(value);
return;
}
flag = true;
console.log(fn.constructor.name === 'AsyncFunction');
if (fn.constructor.name === 'AsyncFunction') {
//@ts-ignore
await fn.apply(this, arguments).finally(() => {
flag = false;
})
} else {
fn.apply(this, arguments);
flag = false;
}
return target;
}
}
}
在class组件中的使用:
class ChekcButton1 extends Component<{}, {}> {
constructor(props: {}) {
super(props)
this.state = {
}
}
private getData = (timer: number): Promise<Number> =>
new Promise((resolve) => {
setTimeout(() => {
resolve(timer);
}, timer)
})
@lockButton('异步buttton请求中')
async getValue1() {
const value = await this.getData(5000);
console.log(value);
if (value > 500) {
console.log('判断');
}
}
render() {
return (
<>
<div>
测试class组件的button装饰器:
<div>
<Button onClick={this.getValue1.bind(this)} type="primary">测试button</Button>
</div>
</div>
</>
)
}
}
总结了以上两种方式不管使用装饰器或者是高阶函数的方式都可以做到对按钮的点击进行控制,但是究其根本还是通过定义变量控制的,所以自己也可以在其他框架中进行探索。
- 在vue中的尝试:同样绘制一个基础的页面一个按钮一个输入框模拟发送请求:
<template>
<div id="app">
<el-input v-model="input" placeholder="请输入内容" class="inputVDom"></el-input>
<el-button type="primary" @click="getValue">按钮</el-button>
</div>
</template>
<script>
import { buttonLock } from "../../util/util";
export default {
name: "buttonLock",
data() {
return {
input: "",
copyValue: "",
};
},
methods: {
getData: function (value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!value) {
this.$message({
message: "'this needs some value!'",
type: "warning",
});
reject("this needs some Value!");
return;
}
this.copyValue = value;
resolve("success");
}, 3000);
});
},
getValue: async function() {
try {
const value = await this.getData(this.input);
console.log(value);
console.log(this.input)
}catch(error) {
console.log(error);
}
},
},
};
</script>
- 同样的情况出现了,请求返回之前重复点击就会重复执行。同样的方法使用高阶函数给他包装起来。
//utils类中方法
const buttonLock = (buttonEvent) => {
let flag = false;
console.log(buttonEvent)
return async function() {
if(flag) {
console.log('loading')
return;
}
flag = true;
await buttonEvent.apply(this,arguments).finally(() => {
flag = false;
})
}
}
//使用:
methods:{
getValue: buttonLock(async function() {
try {
const value = await this.getData(this.input);
console.log(value);
console.log(this.input)
}catch(error) {
console.log(error);
}
})
}