配合 axios 实现防重提交

配合 axios 实现防重提交

防重提交是个老生常谈的问题,使用外部变量锁定或修改按钮状态的方式方式比较繁琐冗余,而知乎的哥们在怎样防止重复发送 Ajax 请求?的问答上,提到了防重提交的几个方式,根据实际项目的需求,采用了A. 独占型提交 + D. 懒惰型提交组合方式,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// http.js

import { debounce } from "../debounce";

let pendingArr = [];
let CancelToken = axios.CancelToken;
let pendingHandler = (flag, cancelFn) => {
if (pendingArr.indexOf(flag) > -1) {
if (cancelFn) {
cancelFn(); // cancel
} else {
pendingArr.splice(pendingArr.indexOf(flag), 1); // remove flag
}
} else {
if (cancelFn) {
pendingArr.push(flag);
}
}
};
// request interceptor
axios.interceptors.request.use(
config => {
config.cancelToken = new CancelToken(cancelFn => {
pendingHandler(config.baseURL + config.url + "&" + config.method, cancelFn);
});
return config;
},
err => {
return Promise.reject(err);
}
);

// response interceptor
axios.interceptors.response.use(
response => {
pendingHandler(response.config.url + "&" + response.config.method);
return response;
},
err => {
pendingArr = [];
return Promise.reject(err);
}
);

return debounce(
axios(config)
.then(response => {
// handle response
resolve(response.data)
})
.catch(thrown => {
if (axios.isCancel(thrown)) {
console.log("Request canceled", thrown.message);
} else {
let { response, request, message } = thrown;
reject(message);
}
}),
500,
true
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// debounce.js
export function debounce(func, wait, immediate) {
var timeout, args, context, timestamp, result;
if (null == wait) wait = 100;

function later() {
var last = Date.now() - timestamp;

if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
context = args = null;
}
}
}

var debounced = function() {
context = this;
args = arguments;
timestamp = Date.now();
var callNow = immediate && !timeout;
if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}

return result;
};

debounced.clear = function() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};

debounced.flush = function() {
if (timeout) {
result = func.apply(context, args);
context = args = null;

clearTimeout(timeout);
timeout = null;
}
};

return debounced;
}

这里用到了 axios 拦截器,初始化一个pendingArr数组,用于存放正在 pending 的请求,以url+method为标志,在 http response 拦截器删除完成 pending 的请求,在 http request 的拦截器中,先判断是否存在正在 pending 的同一个请求,若无则把请求标志位存入数组,若存在,则取消请求。

到这里还未结束,前面做的是终止请求,但是请求已经发出,短时间内频繁请求,会对服务器造成一定压力,所以我这里用了一个debounce (防抖动),规定时间内把触发非常频繁的事件合并成一次执行,比如我这里是500ms 内,触发很频繁的请求都会合并成一次执行,避免用户疯狂点击,触发多次请求的情况。

至于debounce的实现,我这里是摘取underscore源码,至于原理可以参考Debouncing and Throttling Explained Through Examples,这里阐述了 debounce (防抖动)throttling(节流阀)的原理及其异同。