a标签/js 下载服务器文件
- 一、二进制式下载
- 1、responseType(请求)
- 2、Content-Type(响应)判断是普通数据还是文件流(可选)
- 3、Content-Disposition(响应)和文件名(可选)
- 4、文件下载
- 二、URL下载
- 1、a标签的href属性
- 2、a标签的download属性
- 3、下载文件
- 4、download属性的限制
- 5、跨域文件下载解决方案-后端
- 6、跨域文件下载解决方案-前端(鸡肋)
- 7、跨域文件下载解决方案-前端(有效)
主要参考【前端:下载文件实现方式及跨域下载(详解) https://blog.csdn.net/qq_43471802/article/details/103436595】
加上自己遇到的问题,记录并分享,如有错误,请指正
下载文件根据后端返回的是文件流还是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(响应)和文件名(可选)
还是针对第2节所描述的情况:判断出来什么时候是文件数据,在回包里拿到整个文件,而文件名就需要从响应头里的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字段。这个问题会在我的另一篇文章里做记录【js无法获取响应header的Content-Disposition字段(2020)】
4、文件下载
上面第2、3节,在响应拦截器里判断是文件下载并拿到文件名,都不是必须的步骤,只因为我的项目用的axios,并且对axios实例有封装,请求文件的协议也需要和其他普通协议一样带一些参数和配置,才有区分判断的需要
下面就是正式处理响应数据(文件流)并下载文件的方法。
服务返回文件流数据(blob对象),需要用JS对象Blob
构造函数来接收并储存,然后用URL.createObjectURL
生成一个可使用的URL地址,之后把这个URL地址赋给一个临时创建的a标签,用a
标签HTML5新属性download
实现本地储存,以达到实现下载需求:
/*** 下载文件* @param data 二进制文件流数据* @param filename*/
const downloadByFile= function (data, filename) {if (!data) returnlet url = window.URL.createObjectURL(new Blob([data]))let link = document.createElement('a')link.style.display = 'none'link.href = urllink.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、下载文件
使用a标签下载文件,有两种实现:
- 可以直接在html里写个a标签:
<a href="${fileUrl}" download>下载文件</a>
- 对于动态数据,可在js里用api创建a标签:
/*** 下载文件* @param url* @param filename*/
function downloadFile(url,filename) {if (!url) returnlet link = document.createElement('a') //创建a标签link.style.display = 'none' //使其隐藏link.href = url //赋予文件下载地址link.setAttribute('download',filename) //设置下载属性 以及文件名document.body.appendChild(link) //a标签插至页面中link.click() //强制触发a标签事件document.body.removeChild(link);
}
注意!!!!:这里注意a元素的href直接是url字符串,而上面【一、二进制式下载】下载方法里,a元素的href是blob对象对象通过createObjectURL转化得到的可用url。
4、download属性的限制
如果加上download属性,文件还是直接打开,无法正常下载,这有可能是download属性失效造成的。
download属性也受同源策略的影响,即非同一端口下不能直接下载第三方文件,所以这里download失效之后做的仅仅是跳转功能:
所以上面的下载文件方法并不适用于下载跨域文件。
5、跨域文件下载解决方案-后端
针对跨域文件下载问题,可以前端仍是采用上面的方法,后端 oss批量设置HTTP头,设置HTTP请求头为Content-Disposition
为 attachment即可,访问的时候就是直接下载而不是浏览!
这种方法是在参考的文章里提到的,我没测过,不知道可行性,因为url是文件服务器的地址,后端反馈说发送协议可以设置HTTP请求头,但如果没发协议无法对文件服务器如此设置。所以如果有谁知道这种方案,可以交流下
6、跨域文件下载解决方案-前端(鸡肋)
可以对文件类型判断,如果不是图片、文本文件,上面的方法不用加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;
}
但是这种方法,好像也有限制,如果不行就没办法了,这里列出只做记录参考。而且txt格式的文件依然无法下载
7、跨域文件下载解决方案-前端(有效)
因为即使是跨域文件,将该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) returnlet url = window.URL.createObjectURL(new Blob([data]))let link = document.createElement('a')link.style.display = 'none'link.href = urllink.setAttribute('download', filename)document.body.appendChild(link)link.click()document.body.removeChild(link);
}
这个方法亲测有效,暂时没有遇到什么问题。
用axios的项目此时注意,这个get请求应是不需要token之类的,如果底层封装过的axios实例里拦截器各种加东西判断处理,这里就不用和其他的普通请求共用一个封装过的axios实例,使用最原始的axios实例即可,避免拦截器里的处理对它造成影响。