前端并发请求控制:必要性与实现策略
一、引言
在现代 Web 开发中,处理大量异步请求是一个常见场景。虽然浏览器和服务器都有其并发限制机制,但前端实现并发控制仍然具有其独特的价值和必要性。本文将深入探讨这个话题。
二、现有的并发限制机制
2.1 浏览器限制
浏览器对并发请求有默认的限制:
// 主流浏览器的并发限制(同一域名)
const browserLimits = {
Chrome: 6,
Firefox: '6-8',
Safari: 6,
Edge: 6
};
// 但这个限制是按域名的
const requests = [
'https://api1.example.com/data', // 域名1:可以6个并发
'https://api2.example.com/data', // 域名2:可以6个并发
'https://api3.example.com/data' // 域名3:可以6个并发
];
2.2 服务器限制
服务器通常通过限流来控制并发:
// 典型的服务器限流响应
{
status: 429,
message: 'Too Many Requests',
retry_after: 30
}
三、为什么需要前端并发控制
3.1 资源优化
// 不控制并发的问题
urls.forEach(async url => {
try {
await fetch(url);
} catch (e) {
if (e.status === 429) {
// 1. 请求已经发出,消耗了资源
// 2. 服务器已经处理了请求
// 3. 网络带宽已经使用
retry(url);
}
}
});
// 控制并发的优势
await fetchWithLimit(urls, 5);
// 1. 预防性控制,避免资源浪费
// 2. 减少服务器压力
// 3. 优化网络资源使用
3.2 更好的用户体验
// 带进度反馈的并发控制
async function fetchWithProgress(urls, limit = 5) {
let completed = 0;
const total = urls.length;
return fetchWithLimit(urls, limit, {
onProgress: () => {
completed++;
updateUI(`进度:${completed}/${total}`);
},
onError: (error, url) => {
showError(`请求失败:${url}`);
}
});
}
四、实现并发控制
4.1 基础实现
async function fetchWithLimit(urls, limit = 5) {
const results = [];
const executing = new Set();
for (const url of urls) {
const promise = fetch(url).then(response => {
executing.delete(promise);
return response;
});
executing.add(promise);
results.push(promise);
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
4.2 增强版实现
class RequestController {
constructor(options = {}) {
this.limit = options.limit || 5;
this.timeout = options.timeout || 5000;
this.retries = options.retries || 3;
this.queue = [];
this.executing = new Set();
}
async addRequest(url, options = {}) {
const request = {
url,
priority: options.priority || 0,
retryCount: 0
};
this.queue.push(request);
this.processQueue();
}
async processQueue() {
if (this.executing.size >= this.limit) {
return;
}
// 按优先级排序
this.queue.sort((a, b) => b.priority - a.priority);
while (this.queue.length && this.executing.size < this.limit) {
const request = this.queue.shift();
this.executeRequest(request);
}
}
async executeRequest(request) {
const promise = this.fetchWithTimeout(request)
.catch(error => this.handleError(request, error));
this.executing.add(promise);
promise.finally(() => {
this.executing.delete(promise);
this.processQueue();
});
return promise;
}
async fetchWithTimeout(request) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(request.url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
async handleError(request, error) {
if (request.retryCount < this.retries) {
request.retryCount++;
this.queue.unshift(request);
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, request.retryCount) * 1000)
);
} else {
throw error;
}
}
}
五、实际应用场景
5.1 文件上传
class FileUploader {
constructor(options = {}) {
this.controller = new RequestController(options);
this.chunkSize = options.chunkSize || 1024 * 1024;
}
async uploadFile(file) {
const chunks = this.splitFile(file);
const uploads = chunks.map((chunk, index) => ({
url: '/upload',
body: chunk,
priority: chunks.length - index // 优先上传文件的前面部分
}));
return this.controller.addRequests(uploads);
}
}
5.2 数据批量处理
class DataProcessor {
constructor(options = {}) {
this.controller = new RequestController(options);
}
async processDataset(items) {
const batches = this.createBatches(items, 100);
return this.controller.addRequests(batches.map(batch => ({
url: '/process',
body: batch,
priority: batch.some(item => item.isUrgent) ? 1 : 0
})));
}
}
六、最佳实践建议
动态调整并发数:
function getOptimalConcurrency() {
const connection = navigator.connection;
if (!connection) return 5;
switch (connection.effectiveType) {
case '4g': return 6;
case '3g': return 4;
case '2g': return 2;
default: return 3;
}
}
实现优先级控制:
const priorities = {
HIGH: 3,
MEDIUM: 2,
LOW: 1
};
requests.sort((a, b) => b.priority - a.priority);
错误处理和重试策略:
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await delay(Math.pow(2, i) * 1000);
}
}
}
七、总结
前端并发控制虽然看似多余,但实际上是提升应用性能和用户体验的重要手段:
优势:
- 预防性能问题
- 提供更好的用户体验
- 更灵活的控制策略
- 更优雅的错误处理
实现考虑:
- 动态并发数调整
- 请求优先级
- 超时处理
- 错误重试
- 进度反馈
应用场景:
- 文件上传下载
- 数据批量处理
- API 批量调用
- 资源预加载
通过合理的并发控制,我们可以在保证应用性能的同时,提供更好的用户体验。这不仅仅是一个技术问题,更是一个用户体验和资源优化的问题。