与其生成zip文件并从服务器传输,不如下载数据并在浏览器中压缩如何?我最近从事一个可以根据用户请求生成报告的副项目。对于每个请求,我们的后端将生成一份报告,将其上传到AmazonS3存储,并将其URL返回给客户端。由于生成报告需要一些时间,输出文件将被存储,其URL将通过请求参数由服务器缓存。如果用户订购相同的商品,后端将返回现有文件的URL。几天前,我有一个新需求,我需要下载一个包含数百个报告的zip文件,而不是一个文件。我想到的第一个解决方案是:在服务器上准备zip文件上传到AmazonS3存储给客户端下载URL但是这个解决方案有一些缺点:生成zip文件的逻辑非常复杂。我需要考虑为每个请求生成所有文件,或者结合重用现有文件和生成新文件。这两种方法看起来都很复杂。他们将需要一些时间来处理,并且以后需要大量的编码、测试和维护。它无法利用我构建的内容。尽管zip文件是不同的报告集,但很可能大多数单独的报告都是由较早的请求生成的。因此,虽然zip文件本身不太可能重复使用,但各个文件可以。使用上面的方法,我需要一直重做整个过程,效率不是很高。生成zip文件需要很长时间。由于我的后台是单线程进程,这个操作可能会阻塞一段时间其他请求,这段时间可能会超时。在客户端跟踪过程非常困难,我喜欢在网站上有一个进度条。如果一切都在后端处理,我需要找到其他方式向前端报告状态。这并不容易。我想节省基础设施的成本。如果我们可以将一些计算转移到前端并降低基础设施的成本,那就太好了。我的客户不介意再等几秒钟,或者在笔记本电脑上多花MB的内存。我想出的最终解决方案是:将所有文件下载到浏览器中,然后压缩它们。在这篇文章中,我将向您展示如何操作。免责声明:在本文中,我假设您已经具备有关Javascript和Promises的基本知识。如果您还没有,我建议您在返回这里之前了解它们:)下载单个文件我的系统允许在应用新解决方案之前下载报告文件。有很多方法可以做到这一点,后端可以直接响应原始文件内容的HTTP请求,也可以将文件上传到另一个存储设备并返回文件URL。我选择第二种方法是因为我想缓存所有生成的文件。有了文件URL后,在客户端工作就非常简单:在新选项卡中打开此URL。浏览器将完成其余的工作以下载文件。constdownloadViaBrowser=url=>{window.open(url,'_blank');}下载多个文件并存储在内存中当下载并压缩多个文件时,我们不能再使用上面的简单方法了。如果一个JS脚本试图同时打开多个链接,浏览器会怀疑它是否存在安全威胁并警告用户阻止这些操作。虽然用户可以确认继续,但这不是一个好的体验你无法控制下载的文件,浏览器管理文件内容和位置解决这个问题的另一种方法是使用fetch下载文件并将数据存储为内存中的一个Blob。然后我们可以将其写入文件或将这些blob数据合并到一个zip文件中。constdownload=url=>{returnfetch(url).then(resp=>resp.blob());};此函数返回解析为blob的承诺。我们可以结合Promise.all()来下载多个文件。Promise.all()将立即履行所有承诺,解决所有子承诺是否已解决,或者其中一个承诺是否有错误。constdownloadMany=urls=>{returnPromise.all(urls.map(url=>download(url))}按X个文件组下载但是如果我们需要一次下载很多文件怎么办?比如说1000个文件?使用Promise。all()可能不再是一个好主意,你的代码将一次发送一千个请求。这种做法有很多问题:操作系统和浏览器支持的并发连接数是有限的。因此,浏览器可以只处理几个请求。其他请求被放入一个队列,超时计数。结果,你的大部分请求在发送之前都会超时。一次发送很多请求也会使后端超载。我的一个解决方案考虑的是将文件分成多个组。假设我有1000个文件要下载。我不会立即开始通过Promise.all()一次下载所有文件,而是一次下载5个文件。完成这5个后,我再开一个包,一共下载250个包。为了实现这个功能,我们可以做一个自定义逻辑。或者我建议一个更简单的方法,就是使用第三方库bluebirdjs。这个库实现了许多有用的Promise函数。在此用例中,我将使用Promise.map()。注意,这里的Promise现在是库提供的自定义Promise,而不是内置的Promise。importPromisefrom'bluebird';constdownloadByGroup=(urls,files_per_group=5)=>{returnPromise.map(urls,asyncurl=>{returnawaitdownload(url);},{concurrency:files_per_group});}通过上面的实现,这个函数将接收一个url数组并开始下载所有url,每次使用maxfiles_per_group。该函数返回一个Promise,它将在下载所有URL时解析,并在其中任何一个失败时拒绝。创建zip文件现在我已将所有内容下载到内存中。正如我上面提到的,下载的内容存储为一个blob。下一步是用这些blob数据创建一个压缩文件。importJsZipfrom'jszip';importFileSaverfrom'file-saver';constexportZip=blobs=>{constzip=JsZip();blobs.forEach((blob,i)=>{zip.file(`file-${i}.csv`,blob);});zip.generateAsync({type:'blob'}).then(zipFile=>{constcurrentDate=newDate().getTime();constfileName=`combined-${currentDate}.zip`;returnFileSaver.saveAs(zipFile,fileName);});}最终代码让我们在这里完成我为此完成的所有代码。importPromisefrom'bluebird';importJsZipfrom'jszip';importFileSaverfrom'file-saver';constdownload=url=>{returnfetch(url).then(resp=>resp.blob());};constdownloadByGroup=(urls,files_per_group=5)=>{returnPromise.map(urls,asyncurl=>{returnawaitdownload(url);},{concurrency:files_per_group});}constexportZip=blobs=>{constzip=JsZip();blobs.forEach((blob,i)=>{zip.file(`file-${i}.csv`,blob);});zip.generateAsync({type:'blob'}).then(zipFile=>{constcurrentDate=newDate().getTime();constfileName=`combined-${currentDate}.zip`;returnFileSaver.saveAs(zipFile,fileName);});}constdownloadAndZip=urls=>{returndownloadByGroup(urls,5).then(exportZip);}总结在客户端使用Capabilities有时对于减少后端的工作量和复杂性非常有用。不要一次发送大量请求。你会在前端和后端都遇到麻烦。相反,将其分解成更小的部分。引入一些第三方库bluebird、jszip和file-saver。他们对我很好,也可能对你有用。
