前端经常碰到有需要下载文件或图片的业务情况,即后端返回来的是文件流,那么该如何处理之呢?现总结开发流程如下。
const resp = await axios.get('/xxx', {
params: {
url
}
});
后端直接返回来的就是文件流数据,即axios response
对象打印如下。
axios response
对象的data属性打印如下。
在请求的config中添加 responseType: 'blob'
const resp = await axios.get('/xxx', {
params: {
url
},
responseType: 'blob'
});
{
"data": {},
"status": 200,
"statusText": "OK",
"headers": {
"content-type": "text/plain;charset=UTF-8, application/vnd.openxmlformats-officeddocument.spreadsheetml.sheet;charset=UTF-8"
},
"config": {
"url": "xxx",
"method": "get",
"headers": {},
"params": {
"url": "xxx.xlsx"
},
"baseURL": "xxxx",
"transformRequest": [
null
],
"transformResponse": [
null
],
"timeout": 0,
"withCredentials": true,
"responseType": "blob",
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1,
"transitional": {
"silentJSONParsing": true,
"forcedJSONParsing": true,
"clarifyTimeoutError": false
}
},
"request": {}
}
针对Blob类型修改一番响应拦截。
$http.interceptors.response.use(
response => {
console.log('响应拦截');
console.log(response);
// 如果是没有状态码的响应
if (!response.data.code) {
const resType = Object.prototype.toString.call(response.data);
const isBlob = resType === '[object Blob]';
if (isBlob || resType === '[object String]') return response;
}
// 如果有响应的状态码
switch (response.data.code) {
case 2000:
break;
case 1001:
if (router.currentRoute.value.path !== '/Login') {
router
.replace({
path: '/Login',
query: { redirect: router.currentRoute.value.path }
})
.catch(err => {});
}
localStorage.removeItem('SonicToken');
break;
case 1003:
ElMessage.error({
message: $tc('dialog.permissionDenied')
});
break;
default:
if (response.data.message) {
ElMessage.error({
message: response.data.message
});
}
}
return response.data;
},
err => {
ElMessage.error({
message: '系统出错了!'
});
return Promise.reject(err);
}
);
/**
* desc: 文件下载 导出结果处理 type:文件类型 zip .xls .xlsx ...
*/
export function handleDownLoadFile(response, type, fileName) {
let blob = new Blob([response], {
type: type + ';charset=utf-8'
});
let src = window.URL.createObjectURL(blob);
if (src) {
let link = document.createElement('a');
link.style.display = 'none';
link.href = src;
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link); //下载完成移除元素
window.URL.revokeObjectURL(src); //释放掉blob对象
}
}
根据MDN文档:Access-Control-Expose-Headers
默认情况下,header只有六种 simple response headers (简单响应首部)可以暴露给外部:
这里的暴露给外部,意思是让客户端可以访问得到,既可以在Network里看到,也可以在代码里获取到他们的值。
上面问题提到的content-disposition
不在其中,所以即使服务器在协议回包里加了该字段,但因没“暴露”给外部,客户端就“看得到,吃不到”。
而响应首部 Access-Control-Expose-Headers 就是控制“暴露”的开关,它列出了哪些首部可以作为响应的一部分暴露给外部。
所以如果想要让客户端可以访问到其他的首部信息,服务器不仅要在header里加入该首部,还要将它们在 Access-Control-Expose-Headers 里面列出来。
后端设置
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition")
response.setHeader("Content-Disposition", ...)
成功设置后,服务台Network可以看到:
最终,js就能获取到响应header的Content-Disposition
字段的值了。
const netDownLoadFile = async url => {
ElMessage.success({
message: '操作成功'
});
try {
const resp = await axios.get('/xxx', {
params: {
url
},
timeout: 60 * 1000
});
// 处理下载文件逻辑
if (resp.data && resp.data.type === 'application/json') {
ElMessage.error({
message: '操作失败'
});
} else {
// 传回来的是文件
handleDownLoadFileFromHeader(resp);
ElMessage.success({
message: '操作成功'
});
}
} catch (e) {
console.log(e);
ElMessage.error({
message: '操作失败'
});
} finally {
}
};
有时候需要拿到图片,然后从文件流中生成图片url,填入img的src中。
例如,需要上传图片,如果已上传,就展示,未上传,就显示上传按钮。
ant design vue组件调用
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
accept="image/jpeg,image/jpg,image/png"
:beforeUpload="beforeUpload"
:customRequest="uploadImage"
>
<div v-if="imageUrl">
<img style="width: 200px" :src="imageUrl" alt="" />
<a-button style="margin-top: 15px" size="small" @click="e => handlePreview(e, imageUrl)"
>查看大图
a-button>
div>
<div v-else>
<a-icon :type="loading ? 'loading' : 'plus'" />
<div class="ant-upload-text">Uploaddiv>
div>
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancelPreview">
<img alt="example" style="width: 100%" :src="previewImage" />
a-modal>
a-upload>
自定义上传方法
const methods = {
// 上传图片相关
beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png';
if (!isJpgOrPng) {
this.$message.error('只能上传jpg/png格式的图片');
}
const isLt2M = file.size / 1024 / 1024 < 10;
if (!isLt2M) {
this.$message.error('图?不得?于10MB');
}
return isJpgOrPng && isLt2M;
}
// 上传图片的正式请求
uploadImage(file) {
this.netSavPic(params);
setTimeout(() => {
this.init();
}, 500);
},
}
图片业务逻辑
async netGetPic(params) {
this.spinning = true;
let resp,
err = {};
try {
resp = await getPicture(params);
err = resp;
} catch (error) {
err = error;
}
if (resp.data && resp.data.type == 'application/json') {
this.imageUrl = '';
} else {
// 传回来的是图片
const blob = new Blob([resp.data], { type: 'image/png' });
this.imageUrl = window.URL.createObjectURL(blob);
}
this.spinning = false;
},
请求方法
export function getReferencePicture(data, params) {
const form = new FormData();
Object.keys(data).forEach(key => {
form.append(key, data[key]);
});
return axios({
url: `${baseUrl}/getPicture`,
responseType: 'blob', //或者是blob
headers: { 'Content-Type': 'multipart/form-data' },
method: 'post',
data: form,
params
});
}