添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
强悍的创口贴  ·  使用 Java 管理 Blob ...·  2 月前    · 
睿智的牛肉面  ·  python ...·  2 月前    · 
骑白马的香菜  ·  Java ...·  2 月前    · 
冷静的萝卜  ·  Uncaught (in promise) ...·  8 月前    · 
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' ; // 创建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); // 发送完清除掉 复制代码

优点

  • 传统方式,兼容性好,不会出现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 ) { // 如果是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)

  • 能解决不能直接下载浏览器可浏览的文件
  • 可设置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 的内容,并不是百分百能符合你预期的,除非你的文件名全是英文数字。

    我们提取文件名值:

    // 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 ,获取的文件名编码可能会有问题。
  • 自定义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);
    

    未经允许,请勿私自转载

    我的微信公众号