前端js、a标签下载文件

1,821 阅读8分钟

下载文件根据后端返回的是文件流还是URL下载url地址,主要分两种:

  • 二进制式下载
  • URL下载

一、二进制下载

如果后端返回二进制文件流,前端需要使用Blob接收

1、responseType (请求)

首先在前端发送请求时就应在请求头中,用responseType告知服务器需要返回的数据类型,responseType默认是“json”,这里我们请求的是文件流:“blob”。

不同的请求插件设置header的方式不同,用axios来说,axios.post(url, data, config),responseType是在config里设置的(这些设置应该是在底层赋给请求头):

export function download(url, data) {
    return axiosInstance.post(url,data,{
            responseType: 'blob'
        }
    );
}

注:如果这里不定义responseType,下载下来的文件内容会乱码

2、Content-Type(响应)判断是普通数据还是文件流(可选)

服务器返回不同数据,我们会做不同的处理,json我们直接取用,文件流数据需处理后下载。

在axios项目中,一般为了给所有的请求做一些统一处理,比如baseURL、请求带token,回包错误码提示,在底层封装一个axios实例,所有的请求都调用该实例的方法。

这种情况下,文件的请求就有可能和普通数据的请求调用的是同一个实例。直接在总响应拦截器里判断出文件流并执行下载,就不用在每一文件请求协议回调里各自再写一遍执行下载的代码。如何区分响应数据的是文件流还是json数据就很有必要了。

头部Content-Type表示服务端发送的类型及采用的编码方式,一般为application/json.而回包是文件,则Content-Type 一般为“octets/stream”,我们就以此判断是返回的是文件还是普通数据。

//axios响应拦截器里
if(res.headers &&
  (res.headers['content-type'].indexOf('application/x-msdownload') != -1 ||
    res.headers['content-type'].indexOf('octets/stream') != -1 ||
    res.headers['content-type'].indexOf('application/octet-stream') != -1)){
   //执行下载方法
}

3、Content-Disposition(响应)和文件名(可选)

判断出来什么时候是文件数据,在回包里拿到整个文件,而文件名就需要从响应头里的Content-Disposition属性获取

Content-Disposition 文档

Content-Disposition可以出现在消息主体中, 也可以出现在multipart/form-data类型的应答消息体中。Content-Disposition在不同的地方有不同的作用和意义,而文件下载属于前者,下面我们也只说第一种。

在常规的HTTP应答中,Content-Disposition在响应头,有两个参数。第一个参数用于指示回复的内容该以何种形式展示:

inline — 默认值,内联形式。表示回复中的消息体会以页面的一部分或者整个页面的形式展示) attachment— 附件形式。意味着消息体应该被下载到本地,大多数浏览器会自动触发一个“保存为”的对话框,将filename的值预填为下载后的文件名,假如它存在的话 当第一个参数为attachment 时才有第二个参数——filename。

这里Content-Disposition应为attachment,文件名就在第二个参数里:

image.png

我们可以从参数里分离出文件名:

if(res.headers["content-disposition"] && res.headers["content-disposition"] &&  res.headers["content-disposition"].split(";").length > 1 &&
  res.headers["content-disposition"].split(";")[1].split("filename=").length > 1){
    filename = res.headers["content-disposition"].split(";")[1].split("filename=")[1];
    filename = Base64.decode(filename, "utf-8");
}

注: 可能遇到的坑:js代码里无法获取响应header的Content-Disposition字段。

产生上述问题的原因: Access-Control-Expose-Headers

根据MDN文档:Access-Control-Expose-Headers

默认情况下,header只有六种 simple response headers (简单响应首部)可以暴露给外部:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

这里的暴露给外部,意思是让客户端可以访问得到,既可以在Network里看到,也可以在代码里获取到他们的值。

上面问题提到的content-disposition不在其中,所以即使服务器在协议回包里加了该字段,但因没“暴露”给外部,客户端就“看得到,吃不到”。

响应首部 Access-Control-Expose-Headers 就是控制“暴露”的开关,它列出了哪些首部可以作为响应的一部分暴露给外部。

所以如果想要让客户端可以访问到其他的首部信息,服务器不仅要在heade里加入该首部,还要将它们在 Access-Control-Expose-Headers 里面列出来

解决方案

服务端配置

image.png

成功设置后,服务台Network可以看到,js也能获取到响应header的Content-Disposition字段的值了;

4、文件下载

服务返回文件流数据(blob对象),需要用JS对象Blob构造函数来接收并储存,然后用URL.createObjectURL生成一个可使用的URL地址,之后把这个URL地址赋给一个临时创建的a标签,用a标签HTML5新属性download实现本地储存,以达到实现下载需求:

/**
 * 下载文件
 * @param data  二进制文件流数据
 * @param filename
 */
const downloadByFile= function (data, filename) {
    if (!data) return
    
     let url = window.URL.createObjectURL(new Blob([data]))
    let link = document.createElement('a')
    link.style.display = 'none'
    link.href = url
    link.setAttribute('download', filename)

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link);
}

二、URL下载

服务器只是返回文件的url和name;

1、a标签的href属性

这种情况还是借助a标签进行下载,a标签的href属性包含超链接指向的 URL 或 URL 片段。
对于大多数文件,只要用href指向文件url,点击a标签,就会下载文件:

<a href="${fileUrl}">下载文件</a>
// 动态文件 可以按照上文二进制下载方法下载

对于一些浏览器可以识别的文件格式,比如.txt、.png、.jpg 、.mp4等,这样写只会直接在浏览器打开该文件,无法下载。针对这种情况,H5新增了download属性

2、 a标签的download属性

download属性可以指示浏览器下载 URL 而不是导航到它。

如果download属性有一个值,那么此值将在下载保存过程中作为预填充的文件名。

所以只要加上download属性,就可以正常下载文件了。

3、 download属性的限制

如果加上download属性,文件还是直接打开,无法正常下载,这有可能是download属性失效造成的。

download属性也受同源策略的影响,即非同一端口下不能直接下载第三方文件,所以这里download失效之后做的仅仅是跳转功能:

所以上面的下载文件方法并不适用于下载跨域文件。

4、 跨域文件下载解决方案-后端

针对跨域文件下载问题,可以前端仍是采用上面的方法,后端 oss批量设置HTTP头,设置HTTP请求头为Content-Disposition
为 attachment即可,访问的时候就是直接下载而不是浏览!

这种方法是在参考的文章里提到的,我没测过,不知道可行性;

5、 跨域文件下载解决方案-前端(鸡肋)

可以对文件类型判断,如果不是图片、文本文件,上面的方法不用加download属性就是有效的;
如果是图片,可以试试下面的方法:

export function downloadIamge(url,name){
    let image = new Image();
    // 解决跨域 Canvas 污染问题
    image.setAttribute("crossOrigin", "anonymous");
    image.onload = function() {
        let canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        let context = canvas.getContext("2d");
        context.drawImage(image, 0, 0, image.width, image.height);
        let url = canvas.toDataURL("image/png"); //得到图片的base64编码数据
        let a = document.createElement("a"); // 生成一个a元素
        let event = new MouseEvent("click"); // 创建一个单击事件
        a.download = name || "photo"; // 设置图片名称
        a.href = url; // 将生成的URL设置为a.href属性
        a.dispatchEvent(event); // 触发a的单击事件
    };
    image.src = url;
}

6、 跨域文件下载解决方案-前端(有效)

因为即使是跨域文件,将该url输入在浏览器地址栏回车,是可以查看的,打开控制台,可以看到这里是get图片资源显示出来

所以,我们也可以直接以该文件的url发送一个get请求,不通过后端协议,而是直接向文件服务器请求资源。

理论上如果url可以直接查看到文件,那这个get请求就应该也能成功。get请求仍需设置请求头responseType。

这个get请求自然直接返回该文件流,我们用上面【二进制式下载】的方法处理返回结果,就能成功下载文件。这也就是变相使用二进制式下载:

import axios from 'axios'
/**
 * 下载文件
 * @param url 文件url
 * @param fileName
 */
function downloadByURL(url,fileName) {
  axios.get(url, {responseType: 'blob'})
      .then((response) => {
          downloadByFile(response.data,fileName)
      });
}

/**
 * 下载文件
 * @param data  二进制文件流数据
 * @param filename
 */
const downloadByFile= function (data, filename) {
    if (!data) return
    
     let url = window.URL.createObjectURL(new Blob([data]))
    let link = document.createElement('a')
    link.style.display = 'none'
    link.href = url
    link.setAttribute('download', filename)

    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link);
}

用axios的项目此时注意,这个get请求应是不需要token之类的,如果底层封装过的axios实例里拦截器各种加东西判断处理,这里就不用和其他的普通请求共用一个封装过的axios实例,使用最原始的axios实例即可,避免拦截器里的处理对它造成影响。