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
相关产品推荐
相关产品推荐

