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

63次阅读

共计 3482 个字符,预计需要花费 9 分钟才能阅读完成。

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

我最近从事一个副我的项目,该我的项目可依据用户的申请生成报告。对于每个申请,咱们的后端将生成一个报告,将其上传到 Amazon S3 存储,而后将其 URL 返回给客户端。因为生成报告须要一些工夫,因而将存储输入文件,并且服务器将通过申请参数来缓存其 URL。如果用户订购雷同的商品,则后端将返回现有文件的 URL。

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

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

然而此解决方案有一些毛病:

  • 生成 zip 文件的逻辑非常复杂。我须要思考为每个申请生成所有文件,或者在重用现有文件和生成新文件之间进行组合。两种办法仿佛都很简单。他们将破费一些工夫来解决,并且稍后须要大量的编码,测试和保护。
  • 它无奈利用我曾经构建的性能。只管 zip 文件是不同的报告集,但很可能大多数单个报告都是由较早的申请生成的。因而,尽管 zip 文件自身不太可能可重用,但单个文件却能够重用。应用上述办法,我须要始终重做整个过程,这并不是很无效。
  • 生成一个 zip 文件须要很长时间。因为我的后端是一个单线程过程,因而此操作可能会阻止其余申请一段时间,并且在此期间可能会超时。
  • 在客户端跟踪流程十分艰难,我喜爱在网站上搁置进度栏。如果一切都在后端解决,我须要找到其余办法向前端报告状态。这并不容易。
  • 我想节俭基础设施的老本。如果咱们能够将一些计算转移到前端并升高基础架构的老本,那就太好了。我的客户不介意他们再等几秒钟,还是在笔记本电脑上破费额定的 MB RAM。

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

免责申明 :在这篇文章中,我假如你曾经具备无关 Javascript 和 Promise 的基本知识。如果你没有,我倡议你先理解他们,而后再回到这里:)

下载单个文件

在利用新解决方案之前,我的零碎容许下载一个报告文件。有很多办法能够做到这一点,后端能够间接通过 HTTP 申请响应原始文件的内容,也能够将文件上传到另一个存储设备并返回文件 URL。我抉择第二种办法,因为我想缓存所有生成的文件。

一旦有了文件 URL,在客户端上的工作就非常简单:在新选项卡中关上此 URL。浏览器将实现剩下的工作以下载文件。

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

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

当下载和压缩多个文件时,咱们不能再应用下面的简略办法。

  • 如果一个 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);
}

总结

利用客户端的性能有时对于缩小后端的工作量和复杂性十分有用。

不要一次发送大量的申请。你会在前端和后端都遇到麻烦。相同,将作品分成小块。

介绍一些第三方库 bluebird,jszip 和 file-saver。他们为我工作得很好,也可能对您有帮忙:)


本文首发于公众号 《前端全栈开发者》 ID:by-zhangbing-dev,第一工夫浏览最新文章,会优先两天发表新文章。关注后私信回复:大礼包,送某网精品视频课程网盘材料,准能为你节俭不少钱!

正文完
 0