背景
项目中经常会用到下载,大部分时候我们直接使用如下 3 种方式进行下载:
location.href='xxx'
window.open('xxx')
document.createElement('a')
,利用 a 标签download
属性下载
问题
以上 3 种方法能应对大部分场景,但是有以下 3 个致命缺点:
- 不支持
POST
请求😢 - 不支持配置请求头😢
- 不支持异常捕获😢
特别是有复杂导出需求,或者接口和数据不稳定的情况下,导出很容易出错,有时候对请求方式和Content-Type
也会有要求,自定义 headers 是必须的,这时最开始的 3 种方式就做不到了。
分析
考虑到这种下载需求要配置额外的 headers
信息,所以考虑直接用发请求的方式,然后和普通的下载方式结合在一起来实现。经过各种搜索了解了如下一些信息。
Blob
:前端的一个专门用于支持文件操作的二进制对象ArrayBuffer
:前端的一个通用的二进制缓冲区,类似数组,但在 API 和特性上却有诸多不同Buffer
:Node.js 提供的一个二进制缓冲区,常用来处理 I/O 操作
相关的还有 File对象
、FileReader
、TypeArray
等,这里有一张图很清晰的表明了各种对象之间的关系:
通过进一步了解,找到以下对本需求有帮助的点:
1、responseType
responseType
为xhr中的一个属性,可选的参数有:"text"、"arraybuffer"、"blob" 、"document"
,对应的返回数据为 DOMString、ArrayBuffer、Blob、Document
,默认参数为 "text"
。
前端请求二进制数据的时候需要设置数据响应格式:
xhr.responseType = "arraybuffer"
var xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'arraybuffer'
xhr.onload = function(e) {
buffer = xhr.response
console.log(buffer)
};
xhr.send()
2、ArrayBuffer 和 Blob可以互转
- ArrayBuffer转Blob
// arraybuffer转blob很方便,作为参数传入就行了。
var buffer = new ArrayBuffer(16)var blob = new Blob([buffer])
- Blob转ArrayBuffer,需要借助fileReader对象
var blob = new Blob([1,2,3,4,5]);
var reader = new FileReader();
reader.onload = function() {
console.log(this.result);
}
reader.readAsArrayBuffer(blob); // 控制台输出的是ArrayBuffer数据
ArrayBuffer
和 Blob
一样,都是二进制数据的容器,而 ArrayBuffer
相比更为底层,它可以去操作这些二进制数据。
3、window.URL.createObjectURL
Blob
通过 window.URL.createObjectURL
方法可以把一个 blob 转化为一个 Blob URL
,用做文件下载或图片显示的链接,该链接产生于浏览器端,不会占用服务器资源。
下面是一个Blob的例子,可以看到它很短
blob:d3958f5c-0777-0845-9dcf-2cb28783acaf
和冗长的 Base64
格式的 Data URL
相比,Blob URL
的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的 “引用”。从这个角度看,Blob URL
是一个浏览器自行制定的一个伪协议。
解决方案
整理了一下,简单处理了兼容,代码如下:
/**
* 转换并下载
* @param content 返回的数据流
*/
const fn = content => {
const fileData = new Blob([content], {
type: config.fileType
});
// for IE
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(fileData, config.fileName);
}
// for Non-IE (chrome, firefox etc.)
else {
let a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
let url = window.URL.createObjectURL(fileData);
a.href = url;
a.download = config.fileName;
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
};
使用以上代码的前提是请求的响应类型必须为 blob
类型,如下:
由于我们需要支持异常捕获,后台可能返回 json 格式的数据,所以直接用blob会变得难以处理,查阅相关资料,发现可以直接用 arraybuffer
类型,如果后台返回 json 格式,可以转换为 JSON字符串
。
基于以上代码,我优化了一下,让下载支持异常捕获(事实上就是为了捕获异常才搞这么麻烦,不然直接 location.href
打开接口就可以了),由于项目是使用 Vue 开发的,所以我把下载文件流的方法直接放到 vue 的原型上了,用 axios
封装请求,简化调用,终极方法如下:
/**
* 下载文件流,支持异常捕获
* @param config
* {
* method: 请求方法,默认GET
* url: api地址(必传)
* param: 接口参数json
* timeout: 超时时间(毫秒)
* fileName: 文件名,记得带后缀
* fileType: 文件类型,
* successMsg: 成功提示,默认“下载成功”,不传不会显示成功toast
* errMsg: 错误提示,默认“下载失败”
* callback: 回调(type['success'|'error'], message[successMsg|errorMsg])
* }
* @returns {Promise<any>}
*/
Vue.prototype.$downloadStream = config => {
/**
* buffer转换JSON
* @param buffer 二进制数据流
* @returns {JSON}
*/
const bufferToJSON = buffer => {
let array = new Uint8Array(buffer);
let out, i, len, c;
let char2, char3;
out = '';
len = array.length;
i = 0;
while (i < len) {
c = array[i++];
switch (c >> 4) {
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
out += String.fromCharCode(c);
break;
case 12:
case 13:
char2 = array[i++];
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break;
case 14:
char2 = array[i++];
char3 = array[i++];
out += String.fromCharCode(
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
);
break;
}
}
try {
return JSON.parse(out);
} catch (e) {
return {};
}
};
/**
* 转换并下载
* @param content 返回的数据流
*/
const fn = content => {
const fileData = new Blob([content], {
type: config.fileType
});
// for IE
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(fileData, config.fileName);
}
// for Non-IE (chrome, firefox etc.)
else {
let a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
let url = window.URL.createObjectURL(fileData);
a.href = url;
a.download = config.fileName;
a.click();
a.remove();
window.URL.revokeObjectURL(url);
}
};
/**
* 返回Promise对象
*/
return new Promise((resolve, reject) => {
const methods = (config.method || 'get').toLowerCase();
let requestParam = [
config.url,
{
headers: Object.assign({
'X-Requested-With': 'XMLHttpRequest',
returnFullResponse: true // 确保经过axios拦截器后返回完整response,returnFullResponse字段是我自己配置的
},
config.headers
),
timeout: config.timeout || 8000, // 设置超时时间(毫秒),默认8s
// responseType: 'blob', // 不处理异常可直接用blob
responseType: config.responseType || 'arraybuffer', // arraybuffer可以转换为JSON字符串,支持异常捕获
params: methods === 'get' ? config.param : {}
}
];
methods !== 'get' ?
requestParam.splice(
1,
null,
config.headers &&
config.headers['Content-Type'] ===
'application/x-www-form-urlencoded' ?
qs.stringify(config.param) :
config.param
) :
null; // 不同类型,参数位置不同,post类型参数在第2位
axios[methods](...requestParam)
.then(res => {
// console.log('response json: ', res);
let resD = bufferToJSON(res);
// console.log('bufferarray json', resD); // 输出通过arraybuffer得到的json
// 此处需根据项目接口返回结构自行调整,也可以用进行更细的控制
if (resD.code) {
// 失败
const errMsg =
config.errMsg || resD.message || resD.msg || '下载失败!';
// console.error('error catched: ', errMsg);
Vue.prototype.$toast(errMsg, 3); // 我自己封装的toast
config.callback && config.callback('error', errMsg);
reject(errMsg);
} else {
// 成功
fn(res);
const sucMsg = config.successMsg || '下载成功';
config.successMsg && Vue.prototype.$toast(sucMsg);
config.callback && config.callback('success', sucMsg);
resolve(sucMsg);
}
})
.catch(error => {
const errMsg = config.errMsg || error || '下载失败!';
// console.error('error catched: ', errMsg);
Vue.prototype.$toast(errMsg, 3);
config.callback && config.callback('error', errMsg);
reject(errMsg || '下载失败!');
});
});
};
组件中直接在方法中调用,也可以自己封装成 vue指令
,做到 easy use anywhere 😁
/**
* 导出excel
* */
exportExcel() {
// get请求示例
this.$downloadStream({
url: '/api/exportExcel',
fileName: '高二3班学生数据.xlsx'
});
// post请求示例
this.$downloadStream({
url: '/api/exportExcel',
fileName: '高二3班学生数据.xlsx',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
// errMsg: '下载失败!',
// callback: (type, message) => {
// // type:'success'|'error'
// }
});
}
适用场景
- 适用于需要用
GET/POST
方式请求接口进行下载/导出的场景 - 适用于需要考虑异常处理的场景
兼容性
在 IE10, IE11 以及 Microsoft Edge
中 window.URL.createObjectURL()
生成的 blob 链接不能加到 <a>
标签上,也不能直接在浏览器地址栏打开访问,这样会得到 “Error: 拒绝访问” 的错误。IE9 根本不支持 window.URL.createObjectURL
,更别说创建 Blob URL
了。
Microsoft Internet Explorer / Microsoft Edge 和高大上的 Google Chrome / Mozilla Firefox对于 window.URL.createObjectURL
创建 blob 链接最直观的区别在于得到的 blob 链接形式不一样,分别在微软浏览器和标准浏览器中运行以下代码,得到 2 种 blob 链接形式:
- 第一种为
Chrome
和Firefox
生成的带有当前域名的标准blob链接 - 第二种为
Microsoft IE
和Microsoft Edge
生成的不带域名的blob链接
可以通过 window.URL.createObjectURL(new Blob()).indexOf(location.host) < 0
来检测是否是 IE 或早期生成 Object URL
不带域名的 Edge,若表达式返回 true 则为 IE 或 Edge 旧版本。
Blob URL is not supported by IE due to security restrictions.
IE has its own API for creating and downloading files, which is called msSaveOrOpenBlob.
参考文章
axios 请求下载excel文件
聊聊JS的二进制家族:Blob、ArrayBuffer和Buffer
TypeArray、ArrayBuffer、Blob、File、DataURL、canvas的相互转换
评论区