浏览器下载文件的方法总结
背景在lims3项目中,遇到了导入导出的需求,导入较简单,导出分两种,一种是由当前已经拿到的数据生成excel文件,这部分前端借助插件也比较容易完成。但另一种导出,由后台生成文件流,前端接收并下载,我们在这里耗费了较多时间。我们的框架是vue,http请求使用的是axios,很自然地,我们按照后台提供的API进行请求,期望可以返回一个文件地址,或者开始下载文件,并且可以正常打开。然而我们拿到的返回
背景
工作中遇到了导入导出的需求,导入较简单,导出分两种,一种是由当前已经拿到的数据生成excel文件,这部分前端借助插件也比较容易完成。但另一种导出,由后台生成文件流,前端接收并下载,我们在这里耗费了较多时间。
我们的框架是vue,http请求使用的是axios,很自然地,我们按照后台提供的API进行请求,期望可以返回一个文件地址,或者开始下载文件,并且可以正常打开。然而我们拿到的返回结果很奇怪。
看着是一堆乱码。
后来知道这是文件转成的二进制流。但具体怎样实现下载,开始并没有思路。
a标签的download属性规定下载文件的名称。所允许的值没有限制,浏览器将自动检测正确的文件扩展名并添加到文件 (.img, .pdf, .txt, .html, 等等)。
下载zip文件流当遇到下载失败的时候,就需要将后台错误信息返回给用户看,但是responseType: "blob"格式默认转为二进制。所以当下载错误的时候需要转为json格式,拿到code将错误信息返回给用户.
AJAX无法下载文件的原因,
response 会被jQuery转为string,无法拿到blob对象,也就无法转成我们想要的文件url地址。
下载的实现方式
下载其实是浏览器的内置事件,而能够触发下载事件的浏览器的get请求(iframe、a)、post请求(form)具有以下特点:
- response交由浏览器处理
- response内容的格式不会被更改,可以为二进制文件、字符串等。
浏览器会根据response的格式判断应该读取内容还是下载文件等。
我们要做的,就是想办法找到触发浏览器的get或post请求的方法。
get请求
使用iframe和a标签发起get请求时,其实有两个方面的涵义,一个是直接发出http请求,另一个是传入url地址,
我们先直接使用最简单的,发出http请求。
当接口给我们的是get请求时,直接利用iframe的src属性和a标签的href属性可以发送get请求,从而实现下载。
// iframe
const url = "/lims/export/export/workTemplateExport?"+str;
let iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = url
iframe.onload = function () {
document.body.removeChild(iframe)
}
document.body.appendChild(iframe)
// a标签
const url = "http://172.16.157.172:5000/lims/export/export/workTemplateExport?"+str;
let a = document.createElement('a')
a.style.display = 'none'
a.href = url
a.target = '_self'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
缺点: 1.无法满足POST请求类型;
2.如果请求接口出现报错,无法感知;
3.无法获得请求完成的时机;
4.无法上传token
5.参数通过url传递,如果url长度超过2048会被浏览器截断。
post请求
利用form提交请求
// html
<iframe id="id_iframe" name="nm_iframe" style="display:none;" @load="onLoadIframe"></iframe>
// js
// 提交请求并下载文件
onDownloadExcel(url,params){
var form = document.createElement('form');
<!-- 避免请求结束后页面刷新 -->
form.setAttribute('target', 'nm_iframe'); // iframe
form.setAttribute('method', 'get');
form.setAttribute('action', '/lims'+url);
document.body.appendChild(form);
for (var obj in params) {
if (params.hasOwnProperty(obj)) {
var input = document.createElement('input');
input.tpye = 'hidden';
input.name = obj;
input.value = params[obj];
form.appendChild(input);
}
}
form.submit();
document.body.removeChild(form);
},
// 捕获导出接口的错误提示
onLoadIframe(){
let iframe_obj = document.getElementById("id_iframe");
let pre = iframe_obj.contentWindow.document.getElementsByTagName("pre");
let res = {};
if(pre.length>0){
res = JSON.parse(pre[0].innerText);
if(res.code!='0'){
utils.msg.error(res.message)
}
}
},
其中,iframe在这里起到的作用就是避免请求结束后页面刷新;同时可以获取错误提示。
缺点: 1.无法获得请求完成的时机;
2.无法上传token
如果不需要上传token,表单已经基本可以完成我们的需求了,但还是存在无法获得下载进度的缺陷。
除了以上两种直接触发浏览器请求并下载的方式,还可以通过提交请求,将拿到的blob文件转成url地址,赋值给动态添加的iframe/a,从而触发浏览器的下载。
思路即:
- 发送请求
- 获得response
- 通过response判断返回是否为流文件
- 如果是文件则在页面中插入frame/a标签
- 利用frame/a标签实现浏览器的get下载
XMLHttpRequest原生请求
ajax无法下载文件的原因是返回结果被ajax转为字符串,那么我们可以使用原生的XMLHttpRequest对象发送请求
var xhr = new XMLHttpRequest();
var url = "/lims/export/export/workTemplateExport?"+str;
var params = str;
xhr.open('GET', url, true);
xhr.responseType = "blob"; // 返回类型blob
// 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
xhr.onload = function () {
// 请求完成
if (this.status === 200) {
// 返回200
var blob = this.response;
var name = xhr.getResponseHeader("Content-disposition");
var filename = name.match(/filename=(.*)/)[1];
var reader = new FileReader();
reader.readAsDataURL(blob); // 转换为base64,可以直接放入a表情href
reader.onload = function (e) {
console.log('reader',e)
// 转换完成,创建一个a标签用于下载
var a = document.createElement('a');
// decodeURI(filename) 解码文件名
a.download = decodeURI(filename);
a.href = e.target.result;
document.body.append(a);
a.click();
document.body.removeChild(a)
}
}
};
xhr.send(url);
完美解决我们下载中遇到的很多问题,而且请求与response处理方法在一起,可以封装成一个统一的方法,比较推荐该方法
axios请求拦截
axios也是用XMLHttpRequest封装的,而且区别于ajax,返回结果可以拿到blob对象
// http请求
onDownloadExcel(str){
return this.$axios({
url: "/export/export/workTemplateExport?"+str,
method: "GET",
responseType: 'blob', //返回数据的格式,可选值为arraybuffer,blob,document,json,text,stream,默认值为json
timeout: 10*60*1000
})
}
// 处理responses
$axios.onResponse(response => {
if (response.request.responseType === 'blob') {
if(response.headers['content-disposition']){
// 返回待返还文件
// 提取文件名
const filename = response.headers['content-disposition'].match(/filename=(.*)/)[1]
var blob = response.data;
var reader = new FileReader();
reader.readAsDataURL(blob); // 读取Blob内容并将文件数据转换为base64,可以直接放入a标签href
reader.onload = function (e) {
// 转换完成,创建一个a标签用于下载
var a = document.createElement('a');
// decodeURI(filename) 解码文件名
a.download = decodeURI(filename);
a.href = e.target.result;
document.body.append(a);
a.click();
document.body.removeChild(a)
}
}else{
// 返回错误信息
let data = response.data;
var reader = new FileReader();
reader.readAsText(data, 'utf-8');
reader.onload = () =>{
if(utils.isJSON(reader.result)){
<!-- 解析错误信息 -->
data = JSON.parse(reader.result);
if(data.code != '0'){
utils.msg.error(data.message)
}
}
}
}
}
// 返回promise,调用时便于定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
return Promise.resolve(response)
})
与原生相比,使用axios拦截请求并下载文件稍微麻烦一点,因为请求与response处理分开了。但优点是,封装后调用较为简单,且可以使用axios统一封装的方法,如网络情况较差时,阻止同一地址连续多次发出请求等。
相关概念
Blob
Blob 对象表示一个不可变、原始数据的类文件对象
有两种方式可以将读取Blob对象并下载文件
// FileReader
let blob = response.data;
let reader = new FileReader();
reader.readAsDataURL(blob); // 转换为base64,可以直接放入a标签href
reader.onload = function (e) {
// 转换完成,创建一个a标签用于下载
const a = document.createElement('a');
// decodeURI(filename) 解码文件名
a.setAttribute('download', decodeURI(filename))
// 兼容:某些浏览器不支持HTML5的download属性
if (typeof a.download === 'undefined') {
a.setAttribute('target', '_blank')
}
a.style.display = 'none'
a.href = e.target.result;
document.body.append(a);
a.click();
document.body.removeChild(a)
// window.URL.createObjectURL 将二进制流转为blob
let blob = new Blob([response.data], { type: 'application/octet-stream' })
// 创建新的内存URL并指向File对象或者Blob对象的地址
// 每次都会创建一个新的URL对象,所以使用完成后要注意及时使用URL.revokeObjectURL()来释放这个内存地址
const blobURL = window.URL.createObjectURL(blob)
// 创建a标签,用于跳转至下载链接
const a = document.createElement('a')
a.style.display = 'none'
a.href = blobURL
a.setAttribute('download', decodeURI(filename))
// 兼容:某些浏览器不支持HTML5的download属性
if (typeof a.download === 'undefined') {
a.setAttribute('target', '_blank')
}
// 挂载a标签
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
// 释放blob URL地址 必须步骤
window.URL.revokeObjectURL(blobURL)
Blob()构造函数允许用其它对象创建 Blob 对象。比如,用字符串构建一个 blob:
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)],
{type : 'application/json'});
使用 Blob 创建一个指向类型化数组的URL
var url = URL.createObjectURL(blob);
// 会产生一个类似blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 这样的URL字符串
// 你可以像使用一个普通URL那样使用它,比如用在img.src上。
主要区别:
通过FileReader.readAsDataURL(file)可以获取一段data:base64的字符串
通过URL.createObjectURL(blob)可以获取当前文件的一个内存URL
二进制流
总结
总结了拦截请求并下载文件的几种方法,由简单粗陋到较完善,推荐的几种方式:
1.form+隐藏的iframe(缺点:无法定义请求完成的处理函数);
2.XMLHttpRequest原生请求;
3.axios请求
参考文章
更多推荐
所有评论(0)