文件下载
文件下载
前端从后端下载或导出文件的方法
适用场景: 很多时候,前端存在需要从后端下载文件的情况,典型的就是导出excel表格。
一般存在两种方式:
- 请求接口之后,后端返回文件路径,前端直接下载。
- 请求接口之后,后端以文件流的形式返回给前端,前端再下载到本地。
第一种方式
很简单,请求完接口之后,打开该文件的地址:
window.location.href = res.request.responseURL
responseURL
这个地址其实和接口地址是一样,直接打开它,就能默认下载到本地的下载路径了。
非常方便,但是存在不足,比如不能修改文件的名称。
而且,有些场景使用这种方式是行不通的,比如,很多管理系统,都是需要登录的,既然要登录,那就一般都会需要验证每个请求是否安全,往往需要在header里带上token,后端才会给你这个请求放行。
所以,这种方式,你请求接口之后,转到这个链接,其实就是再请求了一次,这个时候你是不好在请求里带上token的,自然也就拿不到你要的文件。
所以,这个时候就要用第二种方式,以文件流的方式来下载。
第二种方式
第二种方式,就是正常的api请求,后端以文件流的形式发送给前端,前端获取到文件数据之后,在本地模拟一次点击按钮下载,不过这次下载不是再向后端请求一次api,而是把第一次请求api之后,后端返回的文件数据转换成合适的格式之后下载下来。
1 | exportFile(this.queryParam).then(res => { |
补充
不管是第一种方式还是第二种方式,尽量让后端指定好文件的类型。当然,使用第二种方式,前端可以再次指定好文件类型。
这里将获取到的文件内容转换成blob类型的数据,是最常见的下载文件数据的格式,当然还可以使用别的方式。
这里创建a标签取下载文件,还可以用别的方式,或者如果碰到浏览器兼容性的问题,可能需要个性化处理。
.download这里可以拿后端返回的文件名,也可以自己定义文件名,看你自己哪个方便一些。如果后端拿到的文件名是乱码,建议直接在前端定义文件名。
最关键的是,下载文件乱码的问题,很多人碰到,解决方法也很简单。
1
2
3
4
5
6
7
8
9
10
11
12
13export function exportFile (parameter) {
return axios({
url: `${api.file}/export`,
method: 'get',
data: parameter,
header: {
headers: { 'Content-Type': 'application/x-download' }
},
responseType: 'blob'
})
}在请求接口的header里一定要指定
responseType
为blob
,否则把返回的文件数据转换成blob对象,blob是不认识的,就会出现乱码。
前端下载文件的5种方法的对比
form表单提交
这是以前常使用的传统方式,毕竟那个年代,没那么多好用的新特性呀。
道理也很简单,为一个下载按钮添加click
事件,点击时动态生成一个表单,利用表单提交的功能来实现文件的下载(实际上表单的提交就是发送一个请求)
来看下如何生成一个表单,生成怎么样的一个表单:
1 | /** |
优点
- 传统方式,兼容性好,不会出现URL长度限制问题
缺点
- 无法知道下载的进度
- 无法直接下载浏览器可直接预览的文件类型(如txt/png等)
open或location.href
最简单最直接的方式,实际上跟a
标签访问下载链接一样
1 | window.open('downloadFile.zip'); |
当然地址也可以是接口api的地址,而不单纯是个链接地址。
优点
- 简单方便直接
缺点
- 会出现URL长度限制问题
- 需要注意url编码问题
- 浏览器可直接浏览的文件类型是不提供下载的,如txt、png、jpg、gif等
- 不能添加header,也就不能进行鉴权
- 无法知道下载的进度
a标签的download
我们知道,a
标签可以访问下载文件的地址,浏览器帮助进行下载。但是对于浏览器支持直接浏览的txt、png、jpg、gif等文件,是不提供直接下载(可右击从菜单里另存为)的。
为了解决这个直接浏览不下载的问题,可以利用download
属性。
download
属性是HTML5新增的属性,兼容性可以了解下 can i use download
总体兼容性算是很好了,基本可以区分为IE和其他浏览。但是需要注意一些信息:
- Edge 13在尝试下载data url链接时会崩溃。
- Chrome 65及以上版本只支持同源下载链接。
- Firefox只支持同源下载链接。
基于上面描述,如果你尝试下载跨域链接,那么其实download
的效果就会没了,跟不设置download
表现一致。即浏览器能预览的还是会预览,而不是下载。
简单用法:
1 | <a href="example.jpg" download>点击下载</a> |
可以带上属性值,指定下载的文件名,即重命名下载文件。不设置的话默认是文件原本名。
1 | <a href="example.jpg" download="test">点击下载</a> |
如上,会下载了一个名叫test
的图片。
监测是否支持download
要知道浏览器是否支持download
属性,简单的一句代码即可区分
1 | const isSupport = 'download' in document.createElement('a'); |
对于在跨域下不能下载可浏览的文件,其实可以跟后端协商好,在后端层做多一层转发,最终返回给前端的文件链接跟下载页同域就好了。
优点
- 能解决不能直接下载浏览器可浏览的文件
缺点
- 得已知下载文件地址
- 不能下载跨域下的浏览器可浏览的文件
- 有兼容性问题,特别是IE
- 不能进行鉴权
利用Blob对象
该方法较上面的直接使用a
标签download
这种方法的优势在于,它除了能利用已知文件地址路径进行下载外,还能通过发送ajax请求api获取文件流进行下载。毕竟有些时候,后端不会直接提供一个下载地址给你直接访问,而是要调取api。
利用Blob
对象可以将文件流转化成Blob
二进制对象。该对象兼容性良好,需要注意的是
- IE10以下不支持。
- 在Safari浏览器上访问
Blob Url
或Object URL
当前是有缺陷的,如下文中通过URL.createObjectURL
生成的链接。caniuse
官网有指出 Safari has a serious issue with blobs that are of the type application/octet-stream
进行下载的思路很简单:发请求获取二进制数据,转化为Blob
对象,利用URL.createObjectUrl
生成url地址,赋值在a
标签的href
属性上,结合download
进行下载。
1 | /** |
该方法不能缺少a
标签的download
属性的设置。
因为发请求时已设置返回数据类型为Blob
类型(xhr.responseType = 'blob'
),所以target.response
就是一个Blob
对象,打印出来会看到两个属性size
和type
。
虽然type
属性已指定了文件的类型,但是为了稳妥起见,还是在download
属性值里指定后缀名,如Firefox不指定下载下来的文件就会不识别类型。
大家可能会注意到,上述代码有两处注释,其实除了上述的写法外,还有另一个写法,改动一丢丢。
如果发送请求时不设置xhr.responseType = 'blob'
,默认ajax请求会返回DOMString
类型的数据,即字符串。
这时就需要两处注释的代码了,对返回的文本转化为Blob
对象,然后创建blob url,此时需要注释掉原本的const url = URL.createObjectURL(target.response)
。
优点
- 能解决不能直接下载浏览器可浏览的文件
- 可设置header,也就可添加鉴权信息
缺点
- 兼容性问题,IE10以下不可用;Safari浏览器可以留意下使用情况
利用base64
这里的用法跟上面用Blob
大同小异,基本上思路是一样的,唯一不同的是,上面是利用Blob
对象生成Blob URL
,而这里则是生成Data URL
,所谓Data URL
,就是base64
编码后的url形式。
1 | /** |
优点
- 能解决不能直接下载浏览器可浏览的文件
- 可设置header,也就可添加鉴权信息
缺点
- 兼容性问题,IE10以下不可用
关于文件名
有时候我们在发送下载请求之前,并不知道文件名,或者文件名是后端提供的,我们就要想办法获取。
Content-Disposition
当返回文件流的时候,我们在浏览器上观察接口返回的信息,会看到有这么一个header:Content-Disposition
1 | Content-Disposition: attachment; filename=CMCoWork__________20200323151823_190342.xlsx; filename*=UTF-8''CMCoWork_%E4 |
上面的值是例子。
其中包含了文件名,我们可以想办法获取其中的文件名。
我们看到,有filename=
和filename*=
,后者不一定有,在旧版浏览器中或个别浏览器中,会不支持这种形式,filename*
采用了RFC 5987
中规定的编码方式。
所以你要获取文件名,就变成,截取这段字符串中的这两个字段值了。
看上面的例子大家可能发现,怎么值怪怪的。是的,如果名字是英文,那好办, 如果是有中文或者其他特殊符号,是需要处理好编码的
filename
,需要后端处理好编码形式,但是就算后端处理好了,也会应每个浏览器的不同,解析的情况也不同。是个比较难处理好的家伙,所以才有后面的filename*
filename*
,是个现代浏览器支持的,为了解决filename
的不足,一般是UTF-8
,我们用decodeURIComponent
就能解码了,能还原成原本的样子。当然,解码前你要把值中的UTF-8''
这种部分给去掉。
所以,在我们实现之前,我们就要明白,取Content-Disposition
的内容,并不是百分百能符合你预期的,除非你的文件名全是英文数字。
我们提取文件名值:
1 | // xhr是XMLHttpRequest对象 |
上面我们获得了两个文件名name1,name2
,如果两个都存在,那么我们优先取name2
的,因为这个更靠谱,name1
如果包含中文或特殊符号,就有风险还原不了真正的文件名。
自定义header
本质上跟上述的Content-Disposition
差不多,只是我们这里不使用默认的header,我们自己自定义一个response header
,跟后端决定好编码方式返回,前端直接获取这个自定义header,然后使用对应的解码即可,如使用decodeURIComponent
。
但是我们都要知道,在跨域的情况下,前端获取到的header只有默认的6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。
所以你想要获取到别的header,需要后端配合,设置
1 | Access-Control-Expose-Headers: Content-Disposition, custom-header |
这样,前端就能获取到对应暴露的header字段,需要注意的是,Content-Disposition
也是需要暴露的。
重命名
这里额外提供个方法,该方法作用是,当你知道文件的全名(含后缀名),想要重命名,但是得后缀名一样,来获取后缀名。
1 | function findType (name) { |
下载大文件
注意: 如果下载特别大的文件,则上面的就不行了,可能会造成网页崩溃,这里就需要用到下载的库。
FileSaver, 轻松实现下载文件500M
1.安装npm依赖
1 | npm install file-saver --save |
2.引入代码
1 | + import { saveAs } from 'file-saver'; |
3.完整例子
1 | + import { saveAs } from 'file-saver'; |
4.其他问题
下载大文件过程中遇到的其他问题
- axios请求超时,注意配置timeout
- Nginx 响应超时报504 网关超时错误,注意配置Nginx
- 控制台报error response,浏览器请求长时间得不到响应,本地调试代理转发超时造成的,参考
- 文件超过2G的解决方案
前端对于文件流的下载
Stream概念
想象一下,你编写了一个文件,你可以将它保存在硬盘上,也可以将它保存到U盘上,这样简单的操作,其实就是数据的传输。
数据的传输,也就是数据的流动,既然出现了这个流动,那就会有出方向和入方向,就像大部分河流一样,在某个地方冒出了涓涓细流,到最后流入大海(这属于地理知识我们在这里不做过多讨论…),在很多时候,流(Stream)我们代指字节流(Byte Steram),也就是长长的一串字节,除了字节流,我们还可能有视频流、音频流、数据流等等……
流按照处理数据的单位,可以分为字节流和字符流。
字节流的处理单位是字节,通常用来处理二进制文件,例如音乐、图片文件等。而字符流的处理单位是字符,因为Java采用Unicode编码,Java字符流处理的即为Unicode字符,所以在操作汉字、国际化等方面,字符流具有优势。
MIME概念
媒体类型(通常称为 Multipurpose Internet Mail Extensions 或 MIME 类型 )是一种标准,用来表示文档、文件或字节流的性质和格式。它在IETF RFC 6838中进行了定义和标准化。
互联网号码分配机构(IANA)是负责跟踪所有官方MIME类型的官方机构,您可以在媒体类型页面中找到最新的完整列表。
MIME
它的全名叫多用途互联网邮件扩展(Multipurpose Internet Mail Extensions),最初是为了将纯文本格式的电子邮件扩展到可以支持多种信息格式而定制的。后来被应用到多种协议里,包括我们常用的HTTP协议。
MIME
的常见形式是一个主类型加一个子类型,用斜线分隔。比如text/html
、application/javascript
、image/png
等。
Blob
概念-什么是Blob
Blob(Binary Large Object)对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。
Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
从MDN
官方的定义来看,我们大概可以知道这个Blob
是用来干啥的了,它是一个用来读取或操作二进制数据的玩意,这我们就可以来下载后端给我们的文件流了啊
用法
构造函数
1 | var Blob = new Blob( array, options ) |
- array 是一个由ArrayBuffer(二进制数据缓冲区)、ArrayBufferView(二进制数据缓冲区的array-like视图)、Blob、DOMString等对象构成的Array,或者其他类似对象的混合体,它将会被放进Blob。DOMStrings会被编码为UTF-8。
- options 是可选的,它可能会指定如下两个属性:
- type,默认值为 “”,它代表了将会被放入到blob中的数组内容的MIME类型。
- endings,默认值为”transparent”,用于指定包含行结束符n的字符串如何被写入。 它是以下两个值中的一个:
- “native”,代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 “transparent”,代表会保持blob中保存的结束符不变。 🌰 :
1 | var say = {hello: "world"} |
URl对象
我们想要下载这个文件需要通过创建URL对象指定文件的下载链接
创建构造函数
创建新的URL表示指定的File对象或者Blob对象。
1 | objectURL = window.URL.createObjectURL(blob) |
释放对象
这个方法不会像JavaScript
的垃圾回收机制那样帮你处理一些事情,在每次调用createObjectURL
方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL
方法来释放。浏览器会在文档退出的时候自动释放它们,但是为了获得最佳性能和内存使用状况,你应该在安全的时机主动释放掉它们。
1 | window.URL.revokeObjectURL(objectURL) |
使用a标签下载
生成一个a标签
1 | const link = document.createElement('a') |
指定下载链接
1 | link.href = window.URL.createObjectURL(blob) |
设置文件名
1 | link.download = fileName |
触发事件
1 | link.click() |
下载
1 | const foo = { |
兼容IE
在IE中要使用window.navigator.msSaveOrOpenBlob
来处理Blob对象。
1 | window.navigator.msSaveOrOpenBlob(blob, fileName); |
整体代码
1 | /*res是请求后返回的文件流,name是传入的文件名*/ |
下载进度
ProgressEvent
接口是测量如 HTTP 请求(一个XMLHttpRequest
,或者一个 <img>
,<audio>
,<video>
,<style>
或 <link>
等底层资源的加载)等底层流程进度的事件。
ProgressEvent.lengthComputable
只读是一个
Boolean
标志,表示底层流程将需要完成的总工作量和已经完成的工作量是否可以计算。换句话说,它告诉我们进度是否可以被测量。ProgressEvent.loaded
只读是一个
unsigned long long
类型数据,表示底层流程已经执行的工作总量。可以用这个属性和ProgressEvent.total
计算工作完成比例。当使用 HTTP 下载资源,它只表示内容本身的部分,不包括首部和其它开销。ProgressEvent.total
只读是一个
unsigned long long
类型数据,表示正在执行的底层流程的工作总量。当使用 HTTP 下载资源,它只表示内容本身的部分,不包括首部和其它开销。
下面的示例为一个新建的 XMLHTTPRequest
添加了一个 ProgressEvent
,并使用它来显示请求状态。
1 | var progressBar = document.getElementById("p"), |
axios onUploadProgress(post) onDownloadProgres(get)
1 | // `onUploadProgress` 允许为上传处理进度事件 |
axios的onDownloadProgress返回参数的total为0的问题。
原因一、很简单,可能只是后端没有返回content-length,让后端工程师加上就行。
原因二、开启了gzip。开启gzip之后服务器默认就会开启文件分块编码【Transfer-Encoding: chunked】,分块编码把「报文」分割成若干个大小已知的块,块之间是紧挨着发送的。
采用这种传输方式进行响应时,没必要带上Content-Length这个首部信息。因为即使带上了也是不准确的,所以content-length会没有。
解决方式之一:
后端把文件大小存储到其他字段,比如:header[‘x-content-length’];