promise批量上传一百张图片,服务器限制五个

5 阅读3分钟

一、问题本质与核心挑战

需求:将100张图片上传至服务器,但服务器限制最多5个并发请求,需设计前端并发控制方案。
核心挑战

  1. 控制并发数不超过5,避免服务器拒绝请求;
  2. 处理请求队列,确保所有图片按顺序或合理策略上传;
  3. 优化用户体验(进度展示、错误重试)。

二、并发控制的三种实现方案

1. 自定义并发控制函数(推荐方案)

通过维护任务队列和执行中的任务数,实现精确的并发控制:

function uploadImage(file) {
  return new Promise((resolve, reject) => {
    // 模拟上传,随机延迟100-500ms
    const delay = 100 + Math.random() * 400;
    setTimeout(() => {
      if (Math.random() < 0.1) {
        // 10%概率模拟失败
        reject(new Error(`上传失败: ${file.name}`));
      } else {
        resolve(`成功: ${file.name}`);
      }
    }, delay);
  });
}

/**
 * 并发控制函数
 * @param tasks 任务数组(返回Promise的函数)
 * @param concurrency 最大并发数
 */
async function controlConcurrency(tasks, concurrency = 5) {
  const results = [];
  const executing = new Set(); // 存储正在执行的任务
  
  // 执行单个任务
  async function executeTask(task, index) {
    const taskPromise = task();
    executing.add(taskPromise);
    
    try {
      const result = await taskPromise;
      results[index] = { success: true, data: result };
      return result;
    } catch (error) {
      results[index] = { success: false, error: error.message };
      throw error;
    } finally {
      executing.delete(taskPromise);
      // 执行下一个任务
      runNextTask();
    }
  }
  
  // 运行下一个任务
  function runNextTask() {
    const nextTask = tasks.shift();
    if (nextTask && executing.size < concurrency) {
      executeTask(nextTask, results.length);
    }
  }
  
  // 启动初始任务
  while (executing.size < concurrency && tasks.length > 0) {
    runNextTask();
  }
  
  // 等待所有任务完成
  await Promise.all(executing);
  return results;
}

// 使用示例
const imageFiles = Array(100).fill(null).map((_, i) => ({ name: `image${i}.jpg` }));

const tasks = imageFiles.map(file => () => uploadImage(file));
controlConcurrency(tasks, 5)
  .then(results => {
    console.log('所有图片上传完成', results);
    // 统计成功/失败数量
    const successCount = results.filter(r => r.success).length;
    console.log(`成功: ${successCount}/${imageFiles.length}`);
  })
  .catch(error => {
    console.error('上传过程中发生错误', error);
  });

2. 使用Async/Await配合队列(简洁方案)

利用JavaScript异步迭代器和队列实现并发控制:

async function uploadImages(images, concurrency = 5) {
  const results = [];
  const queue = [...images];
  const running = new Set();
  
  // 执行上传任务
  async function processImage() {
    if (queue.length === 0 && running.size === 0) {
      return results;
    }
    
    // 取出下一个图片
    const image = queue.shift();
    if (!image) return processImage();
    
    // 执行上传
    const task = uploadImage(image).then(
      data => {
        results.push({ success: true, data });
        return data;
      },
      error => {
        results.push({ success: false, error: error.message });
        throw error;
      }
    ).finally(() => {
      running.delete(task);
      processImage(); // 处理下一个任务
    });
    
    running.add(task);
    return task;
  }
  
  // 启动多个并发任务
  const initialTasks = Array(concurrency).fill().map(processImage);
  return Promise.all(initialTasks);
}

3. 使用第三方库(如p-queue)

借助成熟的并发控制库简化实现:

import PQueue from 'p-queue';

// 上传函数
function uploadImage(file) {
  return new Promise((resolve, reject) => {
    // 模拟上传逻辑
    setTimeout(() => {
      resolve(`上传成功: ${file.name}`);
    }, 100 + Math.random() * 300);
  });
}

// 批量上传
async function batchUpload(images) {
  // 创建队列,最大并发数5
  const queue = new PQueue({
    concurrency: 5,
    autoStart: true
  });
  
  const results = [];
  
  // 添加任务到队列
  images.forEach((image, index) => {
    queue.add(() => 
      uploadImage(image).then(
        data => {
          results[index] = { success: true, data };
          return data;
        },
        error => {
          results[index] = { success: false, error: error.message };
          throw error;
        }
      )
    );
  });
  
  // 等待所有任务完成
  await queue.onIdle();
  return results;
}

// 使用示例
const imageFiles = Array(100).fill(null).map((_, i) => ({ name: `img${i}.png` }));
batchUpload(imageFiles)
  .then(results => {
    console.log(`成功上传: ${results.filter(r => r.success).length}/100`);
  });

三、问题

1. 问:为什么不使用Promise.all直接上传?

    • Promise.all 会同时执行所有100个任务,远超服务器限制的5个并发,导致:
      1. 大量请求被服务器拒绝(429 Too Many Requests);
      2. 客户端内存占用过高,可能导致浏览器卡顿;
      3. 网络资源竞争,实际上传速度反而变慢。
    • 并发控制的核心是平滑控制请求频率,避免资源浪费和服务器过载。

2. 问:如何优化大文件上传的并发策略?

    1. 动态调整并发数:根据网络状况自动降低并发(如通过navigator.connection检测网络类型);
    2. 优先级队列:重要图片(如封面图)优先上传,普通图片后排队;
    3. 分片上传:大文件拆分为多个分片,每个分片作为独立任务加入队列;
    4. 断点续传:结合并发控制,支持中断后从上次完成的分片继续上传。

3. 问:如何实现上传进度的实时展示?

    1. 任务计数:通过完成数/总数计算总体进度;
    2. 单个任务进度:利用XMLHttpRequest.upload.onprogress事件获取单个文件的上传进度;
    3. 进度合并:维护一个进度对象,实时更新每个文件的进度和总体进度:
      const progress = {
        total: images.length,
        completed: 0,
        details: {} // { fileId: { loaded, total, percent } }
      };
      

四、实战优化:错误处理与用户体验

1. 错误重试机制

function uploadImageWithRetry(file, retries = 3) {
  return new Promise((resolve, reject) => {
    const attempt = async (retry = 0) => {
      try {
        const result = await uploadImage(file);
        resolve(result);
      } catch (error) {
        if (retry < retries) {
          console.log(`重试上传: ${file.name} (${retry + 1}/${retries})`);
          setTimeout(() => attempt(retry + 1), 1000 * (retry + 1)); // 指数退避
        } else {
          reject(new Error(`上传失败(${retries}次重试后): ${file.name}`));
        }
      }
    };
    attempt();
  });
}

2. 取消上传功能

function controllableUpload(file) {
  let isCancelled = false;
  return {
    promise: new Promise((resolve, reject) => {
      const task = uploadImage(file)
        .then(resolve)
        .catch(reject);
      
      // 取消逻辑
      this.cancel = () => {
        isCancelled = true;
        // 这里可添加取消上传的逻辑(如AbortController)
      };
    }),
    cancel() {
      isCancelled = true;
    }
  };
}

五、总结

“处理100张图片的并发上传需解决三个核心问题:

  1. 并发控制:通过任务队列和执行中任务计数,确保同时运行的上传任务不超过5个;
  2. 队列管理:维护待执行任务列表,当前任务完成后自动触发下一个任务;
  3. 体验优化:添加错误重试、进度展示和取消功能,提升用户体验。