下载文件根据后端返回的是文件流还是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可以出现在消息主体中, 也可以出现在multipart/form-data类型的应答消息体中。Content-Disposition在不同的地方有不同的作用和意义,而文件下载属于前者,下面我们也只说第一种。
在常规的HTTP应答中,Content-Disposition在响应头,有两个参数。第一个参数用于指示回复的内容该以何种形式展示:
inline — 默认值,内联形式。表示回复中的消息体会以页面的一部分或者整个页面的形式展示) attachment— 附件形式。意味着消息体应该被下载到本地,大多数浏览器会自动触发一个“保存为”的对话框,将filename的值预填为下载后的文件名,假如它存在的话 当第一个参数为attachment 时才有第二个参数——filename。
这里Content-Disposition应为attachment,文件名就在第二个参数里:
我们可以从参数里分离出文件名:
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 里面列出来
解决方案
服务端配置
成功设置后,服务台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实例即可,避免拦截器里的处理对它造成影响。