相当长一段时间,我做的项目中下载文件都是通过点击<a href="link"></a>这样的链接实现的,这种做法虽然比较简单,但是也存在一定弊端。比如后端接口一旦生成文件失败,点击这个链接后打开的页面就不会自动关闭,带来了很不好的用户体验;下载文件的操作完全可以异步实现,目前这种做法是同步的,显然效率不高。
借助jQuery,axios我们可以轻松发起XHR,这就意味着距离实现下载功能,只差在如何将请求的对象保存在本地这一步。首先回顾服务端是如何将文件转换为流的方式发送到前端的。
服务端生成文件字节流
这里以Python为例。使用BytesIO可以轻松地将磁盘文件转换为内存文件。实际上BytesIO对象的读写过程对用户是透明的。绝大部分利用磁盘文件指针进行读写的情景,都可以使用BytesIO对象代替
1 | from io import BytesIO |
上面这一段是从数据库中查询订单信息,并利用pandas.DataFrame存储为Excel表格。我们注意到,to_excel()方法第一个参数原本应该是文件指针或路径,但是这里是一个BytesIO对象。在视图层,我们将这个对象发送给前端。
1 | def download_all(self, request): |
这个HTTP响应和普通的响应不太一样。最主要的特征是响应头的Content-Type字段值为application/octet-stream,octet是‘八’的意思,简单说就是8比特流,也就是字节流。如果不加上这一字段,浏览器就会将文件按照默认文本编码,将这些字节编码后直接显示在屏幕上,结果就是满屏幕的乱码,没人能看懂。在RFC2046中有关于各种取值的详细规定,以及收到字节流以后应该进行的操作。
The recommended action for an implementation that receives an "application/octet-stream" entity is to simply offer to put the data in a file, with any Content-Transfer-Encoding undone, or perhaps to use it as input to a user-specified process.

至此我们已经知道了最重要的一点,浏览器何时会将响应体直接存储为文件,而不是显示出来,取决于响应头中的一些字段。后续利用axios以及jQuery实现异步文件下载,就是利用了这一特点。
直接使用超链接不行吗?
直接使用超链接,在大多数情况下没问题。但有个别时候却很影响用户体验,因为点击超链接后浏览器往往会跳转打开一个新的窗口或选项卡,在下载开始后,这个页面才能自动关闭。这个过程听起来没什么问题,但是下面这两种情况可能对用户并不友好:
- 服务端实时生成文件,数据量较大时需要等待很久才能开始下载,浪费了等待时间
- 服务端出错,页面将出现用户看不懂的错误信息,并且需要用户手动关闭
- 一些接口在请求时需要附加额外的header,或者使用POST方法,超链接无法实现
上图是真实开发中的接口。很明显后端开发人员想要告知用户下载文件时出错的原因,并且提供了可供前端使用的JSON响应体,但是前端人员只是使用了超链接来处理下载,恰好就遇到了上述的第二个问题。面对突然弹出的错误页面,用户会感到手足无措。
上手异步文件下载
已经有太多成熟的js库可以发起XHR(XMLHttpRequest)请求了,也许XHR这个名字有点陌生,但是说起AJAX(Asynchronous Javascript And XML),大家再熟悉不过了。下面用axios实现异步下载这一过程
1 | request({ |
下面我们深入探访一下这几行代码执行时到底发生了什么

首先可以确定的是,axios支持Promise写法,能够很好地处理请求成功与失败的情况。请求如果成功,我们使用Javascript中的blob对象承接原本请求得到的字节流;如果失败,那么我们可以给用户友好的提示信息。
blob对象内部本质上是二进制流,但是八个一组编排一下,也就是字节流了。在异步文件下载这个过程中,blob对象只是一个中间媒介。

可以看到,在这个过程中我们创建了一个ObjectURL对象,对象URL是实现这一过程的核心,每一个对象url都拥有一个全局唯一的地址,例如上图中的blob:http://localhost:9527/490a7d87-745f-4343-b309-5caeaa5c29cd,他们都唯一对应着一个File对象,File建立在blob之上,也就是刚刚承接先行请求响应体的blob对象。a节点的链接直接对应着ObjectURL对象,在上面的代码中我们模拟点击了这个链接,从而触发浏览器的下载机制,也就实现了异步下载。正是由于模拟点击,而且链接的blob对象已经缓存在本地,所以浏览器不会跳转新的页面,而是直接进行下载。
本文作者:MyTech::Author