下载文件并使用JavaScript将其压缩在浏览器中

与其生成zip文件并从您的服务器进行传输,不以下载数据并将其压缩在浏览器中呢?javascript

我最近从事一个副项目,该项目可根据用户的请求生成报告。对于每一个请求,咱们的后端将生成一个报告,将其上传到Amazon S3存储,而后将其URL返回给客户端。因为生成报告须要一些时间,所以将存储输出文件,而且服务器将经过请求参数来缓存其URL。若是用户订购相同的商品,则后端将返回现有文件的URL。html

几天前,我有一个新要求,我须要下载一个包含数百个报告的zip文件,而不是单个文件。我想到的第一个解决方案是:前端

  • 在服务器上准备压缩文件
  • 上传到Amazon S3存储
  • 给客户端提供下载URL

可是此解决方案有一些缺点:java

  • 生成zip文件的逻辑很是复杂。我须要考虑为每一个请求生成全部文件,或者在重用现有文件和生成新文件之间进行组合。两种方法彷佛都很复杂。他们将花费一些时间来处理,而且稍后须要大量的编码,测试和维护。
  • 它没法利用我已经构建的功能。尽管zip文件是不一样的报告集,但极可能大多数单个报告都是由较早的请求生成的。所以,虽然zip文件自己不太可能可重用,但单个文件却能够重用。使用上述方法,我须要一直重作整个过程,这并非颇有效。
  • 生成一个zip文件须要很长时间。因为个人后端是一个单线程进程,所以此操做可能会阻止其余请求一段时间,而且在此期间可能会超时。
  • 在客户端跟踪流程很是困难,我喜欢在网站上放置进度栏。若是一切都在后端处理,我须要找到其余方法向前端报告状态。这并不容易。
  • 我想节省基础设施的成本。若是咱们能够将一些计算转移到前端并下降基础架构的成本,那就太好了。个人客户不介意他们再等几秒钟,仍是在笔记本电脑上花费额外的MB RAM。

我想出的最终解决方案是:将全部文件下载到浏览器中,而后将其压缩。在这篇文章中,我将介绍如何作。git

免责声明:在这篇文章中,我假设你已经具备有关Javascript和Promise的基本知识。若是你没有,我建议你先了解他们,而后再回到这里:)github

下载单个文件

在应用新解决方案以前,个人系统容许下载一个报告文件。有不少方法能够作到这一点,后端能够直接经过HTTP请求响应原始文件的内容,也能够将文件上传到另外一个存储设备并返回文件URL。我选择第二种方法,由于我想缓存全部生成的文件。npm

一旦有了文件URL,在客户端上的工做就很是简单:在新选项卡中打开此URL。浏览器将完成剩下的工做如下载文件。后端

const downloadViaBrowser = url => {
	window.open(url, ‘_blank’);
}
复制代码

下载多个文件并存储在内存中

当下载和压缩多个文件时,咱们不能再使用上面的简单方法。api

  • 若是一个JS脚本试图同时打开许多连接,浏览器会怀疑它是不是一个安全威胁,并警告用户阻止这些行为。虽然用户能够确认继续,但这不是一个好的体验
  • 你没法控制下载的文件,浏览器管理文件内容和位置

解决此问题的另外一种方法是使用 fetch 来下载文件并将数据做为Blob存储在内存中。而后,咱们能够将其写入文件或将这些Blob数据合并为zip文件。数组

const download = url => {
  return fetch(url).then(resp => resp.blob());
};
复制代码

这个函数返回一个被解析为blob的promise。咱们能够结合 Promise.all() 来下载多个文件。Promise.all() 将一次性完成全部的promise,若是全部的子promise都被解析,或者其中一个Promise出现错误,则进行解析。

const downloadMany = urls => {
  return Promise.all(urls.map(url => download(url))
}
复制代码

按X文件组下载

可是,若是咱们须要一次下载大量文件怎么办?假设有1000个文件?使用 Promise.all() 可能再也不是一个好主意,你的代码将一次发送一千个请求。 这种方法有不少问题:

  • 操做系统和浏览器支持的并发链接数是有限的。所以,浏览器一次只能处理几个请求。其余请求放入队列,而且超时计数。结果是,你的大多数请求在发送以前都会超时。
  • 一次发送大量请求也会使后端过载

我考虑过的解决方案是将文件分红多个组。假设我有1000个文件可供下载。而不是经过 Promise.all() 当即开始一次下载全部文件,我将每次下载5个文件。在完成这5个以后,我将开始另外一个包,我总共会下载250个包。

要实现这个功能,咱们能够作一个自定义逻辑。或者我建议一个更简单的方法,就是利用第三方库bluebirdjs。该库实现了许多有用的Promise函数。在这个用例中,我将使用 Promise.map()。注意这里的Promise如今是库提供的自定义Promise,而不是内置的Promise。

import Promise from 'bluebird';
const downloadByGroup = (urls, files_per_group=5) => {
  return Promise.map(
    urls, 
    async url => {
      return await download(url);
    },
    {concurrency: files_per_group}
  );
}
复制代码

经过上面的实现,该函数将接收一个URL数组并开始下载全部URL,每次都具备最大 files_per_group。该函数返回一个Promise,它将在下载全部URL时解析,并在其中任何一个失败时拒绝。

建立zip文件

如今我已经把全部的内容都下载到内存中了。正如我上面提到的,下载的内容被存储为Blob。下一步是使用这些Blob数据建立一个压缩文件。

import JsZip from 'jszip';
import FileSaver from 'file-saver';
const exportZip = blobs => {
  const zip = JsZip();
  blobs.forEach((blob, i) => {
    zip.file(`file-${i}.csv`, blob);
  });
  zip.generateAsync({type: 'blob'}).then(zipFile => {
    const currentDate = new Date().getTime();
    const fileName = `combined-${currentDate}.zip`;
    return FileSaver.saveAs(zipFile, fileName);
  });
}
复制代码

最终代码

让咱们在这里完成我为此完成的全部代码。

import Promise from 'bluebird';
import JsZip from 'jszip';
import FileSaver from 'file-saver';
const download = url => {
  return fetch(url).then(resp => resp.blob());
};
const downloadByGroup = (urls, files_per_group=5) => {
  return Promise.map(
    urls, 
    async url => {
      return await download(url);
    },
    {concurrency: files_per_group}
  );
}
const exportZip = blobs => {
  const zip = JsZip();
  blobs.forEach((blob, i) => {
    zip.file(`file-${i}.csv`, blob);
  });
  zip.generateAsync({type: 'blob'}).then(zipFile => {
    const currentDate = new Date().getTime();
    const fileName = `combined-${currentDate}.zip`;
    return FileSaver.saveAs(zipFile, fileName);
  });
}
const downloadAndZip = urls => {
  return downloadByGroup(urls, 5).then(exportZip);
}
复制代码

总结

利用客户端的功能有时对于减小后端的工做量和复杂性很是有用。

不要一次发送大量的请求。你会在前端和后端都遇到麻烦。相反,将做品分红小块。

介绍一些第三方库bluebirdjszipfile-saver。他们为我工做得很好,也可能对您有帮助:)


来源:levelup.gitconnected.com,翻译:公众号《前端全栈开发者》

相关文章
相关标签/搜索