【前端并发请求控制:必要性与实现策略】

前端并发请求控制:必要性与实现策略

一、引言

在现代 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 批量调用
  • 资源预加载

通过合理的并发控制,我们可以在保证应用性能的同时,提供更好的用户体验。这不仅仅是一个技术问题,更是一个用户体验和资源优化的问题。

你可能感兴趣的:(计算机网路,前端)