前言
关于取消重复请求,最重要的是这么做的意义,而不在于代码的实现
其实,我觉得,绝大部分能够想到的应用场景,都可以通过防抖、节流方式实现,比如实时搜索,比如重复订单提交、比如上拉获取最新数据等
我们知道,所谓取消请求,是在请求已经发出执行的,只不过取消的及时的话,整个请求过程还没有全部完成,可能只是将请求送达服务端,但还没有接收到服务端的返回结果就取消了,这样也仅仅是不对服务端的结果做处理而已
当然,这么做的好处很明显,比如上拉加载数据,在整个上拉过程中,可能会频繁的发出请求,每次请求返回之后,都要将数据渲染到页面上,频繁的操作会造成页面抖动等问题。如果取消重复请求,则只是发出请求,但是对之前服务端返回的结果不做处理,只对最后一次的返回结果做处理,这样就可以避免这个问题
下面就简单聊聊,更希望有同学能够系统对取消重复请求,从前后端的协调的角度来系统谈一谈,我就算抛砖引玉了
1、请求总是会到达服务端的
本节,只是为了证明一点:取消请求,请求也是会到达服务端的!
这对于通盘考虑如何防止重复提交带来的问题是有意义的
1.1 XMLHtppRequest
我们先使用最基本的 XMLHtppRequest 对象来说明这一点
1、服务端代码
接受一个 get 请求,并返回数据
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors())
app.get('/users', (req, res, next) => {
const obj={
name:'yhb',
age:20
}
console.log('请求已到达')
res.send(obj)
})
2、客户端代码
单击按钮后,发送 ajax 请求,然后取消重复请求
document.querySelector('button').addEventListener('click', () => {
const xhr = new XMLHttpRequest()
const url = 'http://127.0.0.1:3000/users'
xhr.open('get', url)
xhr.onreadystatechange = () => {
console.log(xhr.readyState)
}
xhr.send()
// 使用 about 方法取消重复请求
setTimeout(() => {
xhr.abort()
}, 1000)
})
3、测试
在正常的网络情况下,取消请求是没有意义的,因为网速足够快,服务端也没有设置延时,所以你对按钮的点击速度是没有网络快的
我们可以将浏览器的网络换成“低速3G”,然后再重复点击按钮,发现请求已经取消了
再看服务端
可见,虽然已经取消了请求,但是请求还是都到达了服务端
所以,从这一点来考虑,提交订单这类的业务如果仅仅是通过取消重复请求来避免生成多个订单的情况,绝对是不可行的,而必须要服务端进行幂等性判断等方式,来防止重复订单的生成
4、结论
那么,取消重复请求,也就是 about 方法,到底做了什么事情呢?
下面是 MDN 上关于 about 方法的说明
可见,about 方法不会,也不能阻止请求发出,只是发出后就不管了,不再接受服务端返回的数据
我们通过在不同的网络情况下,看一下输出的结果
正常网络
低速3G
输出的只有 4
通过对照 readyState 属性值的含义,我们明白
如果 readyState 的值,如果只输出一个 4,就证明,整个网络请求,没有经过 获取服务端返回的头部信息和状态信息、下载服务端返回的数据,这两个阶段,而是直接认为请求已经完成,所以我们在网络中观察不到任何东西
1.2 axios 取消重复请求
axios 是现在非常流行的一个用于网络请求的库,如果应用在网页端,其内部调用的是 XMLHttpRequest 对象(我也没去验证最新版是不是这样),所以取消请求,其实内部调用的应该还是 about 方法
下面是代码,具体代码含义,后面会详细介绍,这里只要明白,执行结果与上面一模一样就好了
document.querySelector('button').addEventListener('click', () => {
const CancelToken = axios.CancelToken;
let cancel
axios.get('http://127.0.0.1:3000/users', {
cancelToken: new CancelToken(function executor(c) {
cancel = c
})
}).then(res => {
console.log(res.data)
}).catch(err => {
console.log(err)
})
setTimeout(() => {
cancel('Operation canceled by the user.'); // 取消请求,参数是可选的
}, 1000)
})
1.3 结论
通过上面的代码测试,验证了我们的结论
- 所谓取消请求,只是等请求已经发出,且已经到达服务端之后的操作,并不能阻止请求发送
- 无论是否取消请求,服务端都已经接收到请求信息,当然也包括随请求一起发送的参数信息
- 客户端取消请求,只是取消了接受服务端返回信息的步骤
这个结论一定正确吗?谁知道呢!
大家自己提出意见吧!
2. 取消重复请求
本节结合实时搜索的场景,说明如何通过取消重复请求来避免一些问题
首先要明白,在网络状况良好,或者后端响应很快的情况下,取消重复请求是无效的,因为当你想要取消上次请求的时候,整个网络请求早已经完成
所以,我们下面的案例,是在“低速3G” 这种网络状况不好的情况下运行的,模拟的就是网络延迟、服务端响应慢这种情况下,如何通过取消重复请求,达到页面不抖动的目的
2.1 服务端代码
根据用户的输入返回搜索建议
const express = require('express')
const cors = require('cors')
const app = express()
app.use(cors())
app.get('/search', (req, res, next) => {
const data = [
{ key: '春', result: ['春天', '春光明媚'] },
{ key: '春天', result: ['春风送暖', '春光无限'] },
{ key: '春天的', result: ['春天的风', '春天的风景'] },
{ key: '春天的风', result: ['春天的风是醉人的', '春天的风是芳香的'] }
]
const key = req.query.key
// 筛选
const filter_data = data.filter(item => {
return item.key === key
})
if (filter_data.length === 0) {
return res.send([])
}
console.log(filter_data[0].key)
res.send(filter_data[0].result || [])
})
app.listen(3000, () => {
console.log('server is runnig at http://127.0.0.1:3000')
})
2.2 客户端代码
<template>
<div id="app">
<div>
<input type="text" v-model="key" @keyup="getSuggest" />
</div>
<div class="result">
<div v-for="(item, index) in suggestList" :key="index">
{{ item }}
</div>
</div>
</div>
</template>
<script>
import axios from "./api/axios";
// import axios from "axios";
export default {
name: "App",
data() {
return {
key: "",
suggestList: [],
};
},
methods: {
getSuggest() {
const res = axios.get(`http://127.0.0.1:3000/search`, {
params: {
key: this.key,
},
});
res
.then((response) => {
console.log(response.data);
this.suggestList = response.data;
})
.catch((err) => {
console.log("11");
});
},
},
};
</script>
<style>
</style>
2.3 测试
正常网络下:
- 随着文本框的 key 事件发生,不断的有请求发送,且全部都请求成功
- 文本框下方的搜索建议的显示是没有问题的
低速3G
- 随着文本框的 key 事件发生,不断的有请求发送,且全部都请求成功
- 文本框下方的搜索建议的显示发生了奇怪的现象:在输入过程中,搜索建议列表没有任何显示,但是当搜索结束一段时间后,搜索建议列表如同魔法般的在不断变化
- 因为由于网络关系,请求的响应并不及时,不能及时更新搜索建议列表,但是过段时间后,很多来自服务端的响应几乎同一时刻返回,频繁的更新搜索建议列表,导致搜索建议的显示发生了抖动
- 因为网络较慢,请耐心观察动图
总结:
- 两种情况下都会随着文本框的输入,发送大量请求
- 低速3G网络下,请求的响应并不及时,而是很有可能很多请求的响应非常非常密集的返回
下面我们看看如何在 axios 中取消重复的网络请求,一级取消重复请求后的变化
2.4 axios 取消重复请求
2.4.1 如何判断重复网络请求
如何判断重复请求呢?
请求方式、请求 URL 地址和请求参数都一样时,我们就可以认为请求是一样的
可以在每次发起请求时
- 根据当前请求的请求方式、请求URL地址、请求参数生成一个唯一的 key
- 再为每个请求创建一个专属的CancelToken(取消token)
- 将 key 与 cancel 函数以键值对的形式存储起来
- 当出现重复请求时,使用 cancel 函数取消前面的请求
- 将取消的请求从集合中移除
下面是具体的步骤
2.4.2 创建几个函数
下面三个函数,分别用于
- 根据请求信息生成一个key
- 将生成的key 和对应的 Cancel 函数生成对象存储到 map 中
- 从 map 中删除某个网络请求对象
1、定义函数,根据请求生成key
// 根据请求信息,生成 key
const generateKey = (config) => {
let {
method,
url,
params,
data
} = config;
return [method, url].join("&");
}
上面提到:重复请求的判断标准是:请求地址、请求方式、请求参数一致
所以,上面代码中,我们从 config 对象中结构出 请求地址、请求方式、请求头和请求体中的参数
但是在生成 key 时,两个参数并没有参与,原因在于在实时搜索的场景下,每次的参数一定是不同的,如果将参数也参与到 key 的生成,就构不成重复请求,所以,我们只结合请求方式和请求地址生成 key
做人要灵活
2、添加请求对象到 map 中
/**
* @description 添加请求信息 **/
let pendingRequest = new Map();
function addPendingRequest(config) {
// 1、调用 generateKey 方法生成 key
const requestKey = generateKey(config);
/**
* 2、为本次请求添加 cancelToken(取消令牌),
* 同时将key 和 取消函数构成的对象添加到 map 中
*/
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel);
}
});
}
3、从map 中删除请求对象
/**
* @description 取消重复请求 **/
const removePending = (config) => {
const pendingKey = generateKey(config)
const cancelToken = pendingRequest.get(pendingKey)
console.log(cancelToken)
console.log(pendingRequest)
if (cancelToken) {
cancelToken(pendingKey)
pendingRequest.delete(pendingKey)
}
}
2.4.3 在拦截器中应用
任何请求发出时,首先在请求拦截器中执行 removePendingRequest 函数,此函数判断此次请求是否重复的请求,如果是,就取消请求,并从 map 中将上次i请求信息删除
然后调用 addPendingRequest 方法,为此次请求的配置中加上取消请求函数,并将请求添加到 map 中
任何响应返回时,无论成功还是失败,说明此时请求已经顺利完成,本次请求已经结束,都会调用 removePendingRequest 函数将本次请求信息从 map 中移除,否则下次发送请求时还会认为已经发送了请求,进而去删除它,其实它早是早已经完成的请求,没有任何意义
1、请求拦截器中调用函数
/**
* @description 请求拦截器 **/
axios.interceptors.request.use(
function (config) {
// 检查是否存在重复请求,若存在则取消已发的请求,则取消
removePendingRequest(config);
// 把当前请求信息添加到pendingRequest对象中,
addPendingRequest(config);
return config;
},
function (error) {
return Promise.reject(error);
}
);
2、相应拦截器中删除重复请求
/**
* @description 响应拦截器 **/
axios.interceptors.response.use(
function (response) {
// 对响应数据做点什么
removePendingRequest(response.config);
return response;
},
function (error) {
// 从pendingRequest对象中移除请求
removePendingRequest(error.config || {});
if (axios.isCancel(error)) {
console.log("被取消的重复请求:" + error.message);
}
return Promise.reject(error);
}
);
上面并没有对 axios 中相关代码做详细讲解,因为这都属于套路的东西,自己看文档就能够理解,再加上调试,观察的就会更仔细
2.4.4 测试
正常网络下运行代码,效果与设置重复请求是一样,因为当你想取消上次请求时,上次请求早已经完成了,根本就取消不了的
但是在 低速3G 网络下,在频繁的请求中,前面的请求被取消,只有最后一次请求是成功的
再看一下控制台的输出,结合上面客户端的代码,可以得出结论
通过取消重复请求的方式,虽然不能控制网络请求的次数,但是取消之后,请求就没有响应了,就不会执行上面客户端代码中的 then 方法了,也就不会重新为变量suggestList赋值,自然也就不会执行搜索建议列表的渲染了,自然就没有抖动了
只有最后一次没有被取消的请求,响应到达后执行了 then 函数中的代码,其他都是执行的 catch 中的代码,因为取消请求后就会抛出异常,被我们 catch 到了
3. 总结
① 取消重复请求是有用的,但只在一定的条件下发挥作用,比如网络卡顿,或者服务端反应很慢等情况
② 请求一旦被取消,就不会执行 then 方法,而是执行 catch 方法中代码,而我们更新页面等代码都写到 then 中,自然也就不会导致页面抖动
③ 上面提到的网络卡顿等情况毕竟不是天天发生,正常情况下,取消重复请求无法达到第二点说的情况,因为请求的响应速度快于你取消的速度,所以即使加上了取消重复请求代码,但是当文本框内容发生变化时,仍然会有非常频繁的网络请求,进而有非常频繁的dom更新,所以这里一定要配合防抖或者节流才能够减少请求和dom更新次数
④ 请求即使取消,也是会到达服务端的