最近在开发一些大屏,涉及到比较多的数据来源,接口也没有做聚合,导致页面需要调很多接口来填充页面数据,那么就会有接口并发的问题出现
页面太多接口并发请求会出现什么问题?
- 服务器压力会变大:大量的并发请求会导致服务器的负载增加,从而影响服务器的性能和稳定性。
- 网络拥堵:一个域名最多有 6 个并发请求(不同浏览器可能限制不一样),超过 6 个并发请求会导致网络拥堵,从而影响页面的加载速度和用户体验。
- 响应延迟:由于服务器需要处理大量的并发请求,所以响应延迟会增加,从而影响页面的响应速度和用户体验。
解决方式也有很多,比如:
- 负载均衡:分发请求到多个服务器上
- 聚合接口:将多个接口合并成一个接口,减少接口的并发请求
- CDN 内容分发:通过不同的域名进行请求,从而突破浏览器单个域名并发请求限制
以上都是基于运维和后端的角度,那么前端如何解决呢?
并发请求
思路:设置并发请求限制,用队列存放请求,每次请求会先判断是否超出设置的最大并发请求数,当请求完成后,从队列中取出下一个请求,直到队列中的请求全部完成。
js
export class RequestQueue {
constructor(concurrency = 6) {
this.concurrency = concurrency; // 设置最大并发数,默认为6
this.queue = []; // 存放请求的队列
this.current = 0; // 当前正在执行的请求数
}
// 处理队列中的请求(出队)
dequeue() {
while (this.current < this.concurrency && this.queue.length) {
this.current++;
// 从队列中取出下一个请求并执行
const requestPromiseFactory = this.queue.shift();
requestPromiseFactory()
.then(() => {
// 成功的请求逻辑
})
.catch((error) => {
// 失败
console.log(error);
})
.finally(() => {
this.current--;
this.dequeue();
});
}
}
// 添加请求到队列中(入队)
enqueue(requestPromiseFactory) {
this.queue.push(requestPromiseFactory);
this.dequeue();
}
}
代码解释:
- 构造函数 (constructor):初始化了并发数 (concurrency)、请求队列 (queue) 和当前正在执行的请求数量 (current)。
- 入队方法 (enqueue):将请求添加到队列中,并立即调用 dequeue 方法开始处理队列。
- 出队方法 (dequeue):从队列中取出请求并执行。如果请求成功,执行成功逻辑;如果请求失败,捕获错误并记录。无论成功或失败,最终都会调用 finally 块来减少当前正在执行的请求数量,并继续处理下一个请求。
实际使用
js
const requestQueue = new RequestQueue(6); // 创建一个并发数为6的请求队列
// 模拟一个异步函数
sleep(fn) {
return new Promise(resolve => {
setTimeout(() => {
resolve(fn);
}, 2000);
});
},
// 生成测试请求
const queue = [...Array(20)].map((_, i) => () =>
this.sleep(
axios
.get('/api/test' + i)
.then(r => console.log(i, '成功'))
.catch(e => console.log('失败', i))
)
);
// 添加请求到队列中
for (let i = 0; i < queue.length; i++) {
requestQueue.enqueue(queue[i]);
}
请求覆盖
场景:先后有A、B两个请求,A请求还未返回,B请求已经发起,并且B请求的结果比A先返回,那么A请求就会覆盖B请求的结果,正常要的结果是B的结果覆盖掉A请求的结果
可以用队列来维护请求的顺序,按照队列的顺序发起请求,但这有种"杀鸡用牛刀"的感觉,因为我们完全可以取消之前的请求,用最新的请求结果来赋值
可以通过以下方式解决请求覆盖的问题:
- 时序控制:定全局标识,比如数字,依次累加,每个请求响应中判断当前的标识是否 ≥ 全局标识,是则返回结果,否则不返回结果。
- 取消旧请求:发送新请求时判断是否有旧请求,有则取消旧请求,然后再发送新请求。
方法一:时序控制
js
let requestId = 0; // 全局标识
// 发送请求
function sendRequest() {
const currentRequestId = ++requestId; // 递增全局标识
// 发起请求
axios.get('/api/data')
.then(response => {
// 判断当前请求是否是最新的请求(如果有新的请求那么requestId在新的请求会+1,比当前这个方法的curentRequestId的要大)
if (currentRequestId >= requestId) {
// 处理响应数据
console.log(response.data);
}
})
.catch(error => {
// 处理错误
console.error(error);
});
}
方法二:取消旧请求
js
// 通过axios的cancelToken来取消请求
let cancelToken; // 取消请求的令牌
// 发送请求
function sendRequest() {
// 取消旧请求
if (cancelToken) {
cancelToken.cancel();
}
// 创建新的取消请求的令牌
cancelToken = axios.CancelToken.source();
// 发起请求
axios.get('/api/data', {
cancelToken: cancelToken.token
})
.then(response => {
// 处理响应数据
console.log(response.data);
})
.catch(error => {
// 处理错误
console.error(error);
});
}
js
// 自定义的取消请求函数
let lastCancel = null;
let cancelable = (req, callback) => {
let cb = callback;
req.then(res => {
cb && cb(res);
})
let cancel = () => {
cb = null;
}
return cancel;
}
let sendRequest() {
lastCancel && lastCancel();
lastCancel = cancelable(axios.get('/api/data'), res => {
console.log(res);
})
}