我有一个 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 };
};
for...of
版本工作可靠,但速度较慢,因为它按顺序下载文件。我想了解为什么第一个(并发)功能没有按预期工作。我认为同时运行下载会更有效。
关于为什么会发生这种情况以及如何修复它同时保持并发下载的性能优势有什么见解吗?
我很犹豫是否要回答这个问题,因为我担心这可能存在“神奇数字”的因素
您想要的是控制并发下载的数量,但这并不完全可能,因为
FileSaver.js
无法告诉您文件何时完成下载。然而,也许通过对“神奇数字”进行一些调整,您可以获得一致的(一次比一个更快)结果。
// MAGIC numbers
const afterDownloadDelay = 100;
const concurrency = 10;
async function limitAllSettledConcurrent(items, concurrency, fn) {
const results = new Array(items.length);
const queue = items.map((item, index) => ({ item, index }));
const doFn = async ({ item, index }) => {
try {
const value = await fn(item);
results[index] = { value, status: "fulfilled" };
} catch (reason) {
results[index] = { reason, status: "rejected" };
}
return queue.length && doFn(queue.shift());
};
const slots = queue.splice(0, concurrency).map(doFn);
await Promise.all(slots);
return results;
}
const bulkDownload = async (analyses: FullAnalysesResponse[]) => {
setIsLoading(true);
const results = await limitAllSettledConcurrent(analyses, concurrency, 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 }));
if (afterDownloadDelay) {
await new Promise(resolve => setTimeout(resolve, afterDownloadDelay));
}
});
setIsLoading(false);
results.forEach((result, index) => {
const analysisSerialNumber = analyses[index].deviceSerialNumber;
result.status === 'fulfilled'
? successfulQueries.push(analysisSerialNumber)
: failedQueries.push(analysisSerialNumber);
});
return { failedQueries, successfulQueries };
};