45592
在前端站点上下载文件,这是一个极其普遍的需求,很早前就已经有各种解决方法了,为什么还写这么老的文章,只是最近在带一个新人,他似乎很多都一知半解,也遇到了我们必经问题之“不能下载txt、png等文件”的典型问题,我就给他总结下下载的几个方式。顺便分享出来,也许,真有人需要。
form表单提交
这是以前常使用的传统方式,毕竟那个年代,没那么多好用的新特性呀。
道理也很简单,为一个下载按钮添加
click
事件,点击时动态生成一个表单,利用表单提交的功能来实现文件的下载(实际上表单的提交就是发送一个请求)
来看下如何生成一个表单,生成怎么样的一个表单:
* 下载文件
* @param {String} path - 请求的地址
* @param {String} fileName - 文件名
function
downloadFile
(
downloadUrl, fileName
)
{
const
formObj =
document
.createElement(
'form'
);
formObj.action = downloadUrl;
formObj.method =
'get'
;
formObj.style.display =
'none'
;
const
formItem =
document
.createElement(
'input'
);
formItem.value = fileName;
formItem.name =
'fileName'
;
formObj.appendChild(formItem);
document
.body.appendChild(formObj);
formObj.submit();
document
.body.removeChild(formObj);
复制代码
优点
传统方式,兼容性好,不会出现URL长度限制问题
无法知道下载的进度
无法直接下载浏览器可直接预览的文件类型(如txt/png等)
open或location.href
最简单最直接的方式,实际上跟
a
标签访问下载链接一样
window.open('downloadFile.zip');
location.href = '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
表现一致。即浏览器能预览的还是会预览,而不是下载。
简单用法:
<a href="example.jpg" download>点击下载</a>
复制代码
可以带上属性值,指定下载的文件名,即重命名下载文件。不设置的话默认是文件原本名。
<a href="example.jpg" download="test">点击下载</a>
复制代码
如上,会下载了一个名叫
test
的图片
监测是否支持download
要知道浏览器是否支持
download
属性,简单的一句代码即可区分
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
进行下载。
* 下载文件
* @param {String} path - 下载地址/下载请求地址。
* @param {String} name - 下载文件的名字/重命名(考虑到兼容性问题,最好加上后缀名)
downloadFile (path, name) {
const
xhr =
new
XMLHttpRequest();
xhr.open(
'get'
, path);
xhr.responseType =
'blob'
;
xhr.send();
xhr.onload =
function
(
)
{
if
(
this
.status ===
200
||
this
.status ===
304
) {
if
(
'msSaveOrOpenBlob'
in
navigator) {
navigator.msSaveOrOpenBlob(
this
.response, name);
return
;
const
url = URL.createObjectURL(
this
.response);
const
a =
document
.createElement(
'a'
);
a.style.display =
'none'
;
a.href = url;
a.download = name;
document
.body.appendChild(a);
a.click();
document
.body.removeChild(a);
URL.revokeObjectURL(url);
复制代码
该方法不能缺少
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形式。
* 下载文件
* @param {String} path - 下载地址/下载请求地址。
* @param {String} name - 下载文件的名字(考虑到兼容性问题,最好加上后缀名)
downloadFile (path, name) {
const
xhr =
new
XMLHttpRequest();
xhr.open(
'get'
, path);
xhr.responseType =
'blob'
;
xhr.send();
xhr.onload =
function
(
)
{
if
(
this
.status ===
200
||
this
.status ===
304
) {
const
fileReader =
new
FileReader();
fileReader.readAsDataURL(
this
.response);
fileReader.onload =
function
(
)
{
const
a =
document
.createElement(
'a'
);
a.style.display =
'none'
;
a.href =
this
.result;
a.download = name;
document
.body.appendChild(a);
a.click();
document
.body.removeChild(a);
复制代码
优点
能解决不能直接下载浏览器可浏览的文件
可设置header,也就可添加鉴权信息
兼容性问题,IE10以下不可用
关于文件名
有时候我们在发送下载请求之前,并不知道文件名,或者文件名是后端提供的,我们就要想办法获取。
Content-Disposition
当返回文件流的时候,我们在浏览器上观察接口返回的信息,会看到有这么一个header:
Content-Disposition
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
的内容,并不是百分百能符合你预期的,除非你的文件名全是英文数字。
我们提取文件名值:
const content = xhr.getResponseHeader('content-disposition');
if (content) {
let name1 = content.match(/filename=(.*);/)[1];
let name2 = content.match(/filename\*=(.*)/)[1];
name1 = decodeURIComponent(name1);
name2 = decodeURIComponent(name2.substring(6));
复制代码
上面我们获得了两个文件名
name1,name2
,如果两个都存在,那么我们优先取
name2
的,因为这个更靠谱,
name1
如果包含中文或特殊符号,就有风险还原不了真正的文件名。
非全数字英文的文件名,如果浏览器只支持
filename
,获取的文件名编码可能会有问题。
自定义header
本质上跟上述的
Content-Disposition
差不多,只是我们这里不使用默认的header,我们自己自定义一个
response header
,跟后端决定好编码方式返回,前端直接获取这个自定义header,然后使用对应的解码即可,如使用
decodeURIComponent
。
但是我们都要知道,在跨域的情况下,前端获取到的header只有默认的6个基本字段:
Cache-Control
、
Content-Language
、
Content-Type
、
Expires
、
Last-Modified
、
Pragma
。
所以你想要获取到别的header,需要后端配合,设置
Access-Control-Expose-Headers: Content-Disposition, custom-header
复制代码
这样,前端就能获取到对应暴露的header字段,需要注意的是,
Content-Disposition
也是需要暴露的。
这里额外提供个方法,该方法作用是,当你知道文件的全名(含后缀名),想要重命名,但是得后缀名一样,来获取后缀名。
function findType (name) {
const index = name.lastIndexOf('.');
return name.substring(index + 1);
未经允许,请勿私自转载
我的微信公众号