一、当前场景
现在公司大数据相关管理后台越来越多,目前在前端查询接口获取数据时存在三个现象:
- 大数据接口请求时间长(有的长到30秒以上);
- 并发请求多,一个页面可能会同时发出多个请求(甚至7到8个以上);
- 用户在发送请求过程中,受全局loading影响,在请求过程中无法进行任何操作,用户体验较差;
Queueing时间说明:
关于这个,需要知道一个背景,就是浏览器与同一个域名建立的TCP连接数是有限制的, chrome设置的6个,如果说同一时间,发起的同一域名的请求超过了6个,这时候就需要排队了,也就是这个Queueing时间。
举两个例子:
- 某项目数据概览页有十几个接口,我从app应用A切换到B切换到C,用户想看到不就的概览数据,就必须等【B-数据概览请求】的数据返回后,才开始发送【C-数据概览请求】,用户等待的时间是【B-数据概览请求时间】+【C-数据概览请求时间】
- 比如有一个请求有30秒响应时间,我切换了另一个菜单,这个请求相关的数据其实已经不需要了, 但是浏览器还是在等待此请求的返回,并且在数据返回后做相应的处理;
二、引发的问题
由于前端发送请求没有时序控制,可能会引发以下三个问题:
- 用户等待资源的时间过长;
- 有时组件卸载后数据才返回,还可能触发setState,产生警告报错或者内存溢出(组件加载,发起网络请求,组件卸载,网络请求成功触发setState);
- Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
- Warning: Can’t call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
- 一般来说警告并不会让你的程序崩溃,但是它会降低程序的性能
- 在连续发送同一请求时,如果第二次请求比第一次请求快,那么实际显示的是第一次请求的数据,这就会造成数据和选择的内容不一致的问题
三、请求时序控制之中断请求
发送了多个或多批网络请求,我们要保证能取消无意义的请求,总是最新一次或一批网络请求有效,这就是请求的“中断请求”。
四、解决方案
1. 解决思路 - 除旧迎新
用一个变量存储正在发送请求的实例,当进行某一个操作,将之前存储的请求停止。
2. 理论方法
取消请求的方法,在原生的 XHR 对象里方法为:XMLHttpRequest.abort()。在 axios 里有 cancelToken 的 API 提供完成, 由于咱们项目用的是axios,以下解决方案以axios为例;
// `cancelToken`指定可用于取消请求的取消令牌
1. 您可以使用CancelToken.source工厂创建取消令牌,如下所示:
const CancelToken = axios.CancelToken;
//这里初始化source对象axios
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 中断请求
source.cancel('Operation canceled by the user.');
2. 您还可以通过将执行程序函数传递给CancelToken构造函数来创建取消令牌:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// 执行器函数接收一个cancel函数作为参数
cancel = c;
})
});
// cancel the request
cancel();
----简单描述cancel与source---
// source 方法是CancelToken的一个工厂方法
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token, // CancelToken这个类的一个实例,可以访问到CancelToken内部的promise。
cancel: cancel // promise的then里面调用cancel方法后,会理解执行abort方法取消请求
};
};
源码中当调用cancel方法后,实质上执行原生abort方法取消请求,同时调用reject让外层的promise的reject执行。
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
3. 项目应用场景示例
在service-intercept.js(控制请求拦截的代码文件) — 对请求和响应拦截器进行处理
// 1.定义cancel队列 将想要取消的请求放入队列中
const cancelTokenSources = new Map();
// 2.在请求拦截器里面将请求放入cancel队列
axios.interceptors.request.use(config => {
setUserInfoToParams(config)
// 请求拦截器中将请求加入cancel队列 排除不需要cancel的请求
if (!config.hasOwnProperty('cancelToken')) {
const source = axios.CancelToken.source();
const { cancel } = source
// 加入cancel队列
cancelTokenSources.set(cancel);
config.cancelToken = source.token;
}
return config
}, error => {
loading.hide()
console.error('加载超时')
return Promise.reject(error)
})
// 3. 响应拦截器,如果请求结束了,将请求移出队列
axios.interceptors.response.use(response => {
const { config: { cancelToken } } = response
// console.log('响应拦截器', cancelToken)
// 响应拦截器中将请求删除cancel队列
if (cancelToken) {
cancelTokenSources.delete(cancelToken);
}
...
//4. 模块输出队列变量cancelTokenSources
export default cancelTokenSources
// 5. 在工具函数common.js里将取消队列请求的方法定义:
/**
* 中断进行中的请求-时序控制
*/
breakReq: () => {
for (const [cancelToken, cancel] of cancelTokenSources) {
cancel(cancelToken); // cancel 正在pending的请求
}
}
//6. 场景使用-》切换路由或者切换app,调用breakReq函数
// 取消正在进行中的请求
$common.breakReq()