在前端站点上下载文件,这是一个极其广泛的需求,很早前就已经有各类解决方法了,为何还写这么老的文章,只是最近在带一个新人,他彷佛不少都只知其一;不知其二,也遇到了咱们必经问题之“不能下载txt、png等文件”的典型问题,我就给他总结下下载的几个方式。顺便分享出来,也许,真有人须要。前端
这是之前常使用的传统方式,毕竟那个年代,没那么多好用的新特性呀。ajax
道理也很简单,为一个下载按钮添加click
事件,点击时动态生成一个表单,利用表单提交的功能来实现文件的下载(实际上表单的提交就是发送一个请求)后端
来看下如何生成一个表单,生成怎么样的一个表单:api
/** * 下载文件 * @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'; // 建立input,主要是起传参做用 const formItem = document.createElement('input'); formItem.value = fileName; // 传参的值 formItem.name = 'fileName'; // 传参的字段名 // 插入到网页中 formObj.appendChild(formItem); document.body.appendChild(formObj); formObj.submit(); // 发送请求 document.body.removeChild(formObj); // 发送完清除掉 } 复制代码
最简单最直接的方式,实际上跟a
标签访问下载连接同样跨域
window.open('downloadFile.zip'); location.href = 'downloadFile.zip'; 复制代码
固然地址也能够是接口api的地址,而不单纯是个连接地址。浏览器
咱们知道,a
标签能够访问下载文件的地址,浏览器帮助进行下载。可是对于浏览器支持直接浏览的txt、png、jpg、gif等文件,是不提供直接下载(可右击从菜单里另存为)的。bash
为了解决这个直接浏览不下载的问题,能够利用download
属性。微信
download
属性是HTML5新增的属性,兼容性能够了解下 can i use downloadmarkdown
整体兼容性算是很好了,基本能够区分为IE和其余浏览。可是须要注意一些信息:app
基于上面描述,若是你尝试下载跨域连接,那么其实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'); 复制代码
对于在跨域下不能下载可浏览的文件,其实能够跟后端协商好,在后端层作多一层转发,最终返回给前端的文件连接跟下载页同域就行了。
该方法较上面的直接使用a
标签download
这种方法的优点在于,它除了能利用已知文件地址路径进行下载外,还能经过发送ajax请求api获取文件流进行下载。毕竟有些时候,后端不会直接提供一个下载地址给你直接访问,而是要调取api。
利用Blob
对象能够将文件流转化成Blob
二进制对象。该对象兼容性良好,须要注意的是
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) { // 若是是IE10及以上,不支持download属性,采用msSaveOrOpenBlob方法,可是IE10如下也不支持msSaveOrOpenBlob if ('msSaveOrOpenBlob' in navigator) { navigator.msSaveOrOpenBlob(this.response, name); return; } // const blob = new Blob([this.response], { type: xhr.getResponseHeader('Content-Type') }); // const url = URL.createObjectURL(blob); 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)
。
这里的用法跟上面用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: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
的内容,并非百分百能符合你预期的,除非你的文件名全是英文数字。
咱们提取文件名值:
// xhr是XMLHttpRequest对象 const content = xhr.getResponseHeader('content-disposition'); // 注意是全小写,自定义的header也是全小写 if (content) { let name1 = content.match(/filename=(.*);/)[1]; // 获取filename的值 let name2 = content.match(/filename\*=(.*)/)[1]; // 获取filename*的值 name1 = decodeURIComponent(name1); name2 = decodeURIComponent(name2.substring(6)); // 这个下标6就是UTF-8'' } 复制代码
上面咱们得到了两个文件名name1,name2
,若是两个都存在,那么咱们优先取name2
的,由于这个更靠谱,name1
若是包含中文或特殊符号,就有风险还原不了真正的文件名。
filename
,获取的文件名编码可能会有问题。本质上跟上述的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); } 复制代码
未经容许,请勿私自转载
个人微信公众号