You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Promise.allSettled调用FileSaver saveAs批量下载文件不全问题排查

批量并发下载文件失败的问题排查与修复

问题背景

我写了一个TypeScript函数尝试并发批量下载大量小文档,代码如下:

const bulkDownload = async (analyses: FullAnalysesResponse[]) => {
  setIsLoading(true);

  const promises = analyses.map(async (analysis) => {
    const documentInfo = await getDocumentInfo({ documentId: analysis.downloadFileId! });
    const attachment = await downloadDocument({ documentId: analysis.downloadFileId! });
    FileSaver.saveAs(new File([attachment], documentInfo.name, { type: documentInfo.mimeType }));
  });

  const results = await Promise.allSettled(promises);

  setIsLoading(false);

  results.forEach((result, index) => {
    const analysisSerialNumber = analyses[index].deviceSerialNumber;
    result.status === 'fulfilled'
      ? successfulQueries.push(analysisSerialNumber)
      : failedQueries.push(analysisSerialNumber);
  });

  return { failedQueries, successfulQueries };
};

触发批量下载时,并非所有文件都能成功下载,每次下载数量不一致,且从未下载全部文件。所有API调用都正常,Promise也都显示成功,问题出在FileSaver.saveAs函数上。

换成for...of顺序下载的版本就能正常工作,但速度很慢:

const bulkDownload = async (analyses: FullAnalysesResponse[]) => {
  setIsLoading(true);
  
  const successfulQueries: string[] = [];
  const failedQueries: string[] = [];

  for (const analysis of analyses) {
    try {
      const documentInfo = await getDocumentInfo({ documentId: analysis.downloadFileId! });
      const attachment = await downloadDocument({ documentId: analysis.downloadFileId! });
      FileSaver.saveAs(new File([attachment], documentInfo.name, { type: documentInfo.mimeType }));
      successfulQueries.push(analysis.deviceSerialNumber);
    } catch (error) {
      failedQueries.push(analysis.deviceSerialNumber);
    }
  }

  setIsLoading(false);

  return { failedQueries, successfulQueries };
};

想知道并发版本失效的原因,同时要在保留并发性能的前提下解决问题。

问题原因

这是浏览器对同时触发的下载操作有数量限制导致的:

  • FileSaver.saveAs的底层实现是创建一个隐藏的<a>标签,模拟点击触发下载。
  • 浏览器为了防止恶意网站滥用下载功能、保护用户体验,会限制同时进行的下载任务数量(通常是3-5个)。
  • 并发版本中,所有saveAs几乎同时被调用,超出浏览器限制的下载请求会被直接丢弃,所以你看到Promise都成功了(因为API请求完成了),但实际文件没被下载。

解决方案

核心思路是控制并发下载的数量,既保留API请求的并发优势,又避免触发浏览器的下载限制。

方案1:使用并发限制库(如p-limit)

借助成熟的库控制同时执行的任务数,示例代码:

import pLimit from 'p-limit';

const bulkDownload = async (analyses: FullAnalysesResponse[]) => {
  setIsLoading(true);
  // 限制同时进行3个下载任务,可根据浏览器调整
  const limit = pLimit(3);
  
  const successfulQueries: string[] = [];
  const failedQueries: string[] = [];

  const tasks = analyses.map(analysis => limit(async () => {
    try {
      const documentInfo = await getDocumentInfo({ documentId: analysis.downloadFileId! });
      const attachment = await downloadDocument({ documentId: analysis.downloadFileId! });
      // 这里的saveAs会被限制并发数
      FileSaver.saveAs(new File([attachment], documentInfo.name, { type: documentInfo.mimeType }));
      successfulQueries.push(analysis.deviceSerialNumber);
    } catch (error) {
      failedQueries.push(analysis.deviceSerialNumber);
    }
  }));

  await Promise.allSettled(tasks);
  setIsLoading(false);

  return { failedQueries, successfulQueries };
};

方案2:手动实现分批并发

如果不想引入第三方库,可以手动把任务分成多批,每批并发执行:

const bulkDownload = async (analyses: FullAnalysesResponse[], batchSize = 3) => {
  setIsLoading(true);
  
  const successfulQueries: string[] = [];
  const failedQueries: string[] = [];

  // 分批处理
  for (let i = 0; i < analyses.length; i += batchSize) {
    const batch = analyses.slice(i, i + batchSize);
    await Promise.allSettled(
      batch.map(async analysis => {
        try {
          const documentInfo = await getDocumentInfo({ documentId: analysis.downloadFileId! });
          const attachment = await downloadDocument({ documentId: analysis.downloadFileId! });
          FileSaver.saveAs(new File([attachment], documentInfo.name, { type: documentInfo.mimeType }));
          successfulQueries.push(analysis.deviceSerialNumber);
        } catch (error) {
          failedQueries.push(analysis.deviceSerialNumber);
        }
      })
    );
  }

  setIsLoading(false);
  return { failedQueries, successfulQueries };
};

方案3:先批量下载文件数据,再依次触发下载

适合小文件场景,先把所有文件的内容和信息都下载到内存,再逐个调用saveAs:

const bulkDownload = async (analyses: FullAnalysesResponse[]) => {
  setIsLoading(true);
  
  // 先并发下载所有文件数据和信息
  const fileDataList = await Promise.allSettled(
    analyses.map(async analysis => {
      const documentInfo = await getDocumentInfo({ documentId: analysis.downloadFileId! });
      const attachment = await downloadDocument({ documentId: analysis.downloadFileId! });
      return {
        serialNumber: analysis.deviceSerialNumber,
        file: new File([attachment], documentInfo.name, { type: documentInfo.mimeType })
      };
    })
  );

  // 依次触发下载,避免浏览器限制
  const successfulQueries: string[] = [];
  const failedQueries: string[] = [];
  fileDataList.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      FileSaver.saveAs(result.value.file);
      successfulQueries.push(result.value.serialNumber);
    } else {
      failedQueries.push(analyses[index].deviceSerialNumber);
    }
  });

  setIsLoading(false);
  return { failedQueries, successfulQueries };
};

总结

并发版本失效的核心是浏览器限制了同时下载的数量,通过控制saveAs的并发执行次数,就能在保留API请求并发优势的同时,保证所有文件都能成功下载。

内容的提问来源于stack exchange,提问作者Nerd_Gotti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 11:06:19