导出现状
- 大型项目,有文件管理,前台导出请求会被后台写入到文件管理,可能有权限和记录需求
- 一般项目,直接返回流,但通常会做为两个接口。前端调用导出校验成功后,再调用导出流接口
- 当前方案:后端查询直接成功直接返回blob,异常返回json,后台返回值类型不一致也会报错
Q:问题
后端成功会返回流,失败返回json,但因为 axios 请求时声明了
responseType: 'blob'
,所以返回值会被处理为blob,通过导出方法,会将json直接导出为 Excel文件名处理,以方法传入为优先级,若无取 Content-Disposition 返回 fileName,若无取默认值
A:答案
- 若后台返回不是成功,给出报错提示,而不是直接导出为 Excel(默认是这样子)
- 获取到后台 Header 的 Content-Disposition,作为导出文件名称
S:方案
-
获取到后台 Header 的 Content-Disposition,作为导出文件名称
- 主要是后台调整,java为例。首先要设置header,因为返回流就不会返回json了
- 其次是要设置response header 暴露给前端访问。不设置在浏览器查看有,js访问会为空
备注:设置需要在 write 前赋值
response.reset(); // 重置输出流
response.setContentType("application/vnd.ms-excel;charset=UTF-8"); //通知客服文件的MIME类型
//设置要下载的文件的名称: 若是中文需要转码, java乱码为 ?????
response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(sheetName, "utf-8"));
// 服务端要在header设置Access-Control-Expose-Headers, 前端才能正常获取到
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
- 若后台返回不是成功,给出报错提示,而不是直接导出为 Excel(默认是这样子)- 文件名称处理
2.1 在返回 aop 处理导出
// 响应拦截器
http.interceptors.response.use(
async response => {
function formatResponse(insertFragment) {
_interceptorsLoadingAndMessage();
insertFragment && insertFragment();
return response.data;
}
// 文件Excel导出: NOTE: 处理请求声明的blob是否为json
if (response.config.conf[KEY_EXPORT_TYPE]) {
const res = await _fileToJson(response.data);
if (!res.message) {
response.data = res.data;
} else {
// 处理文件名称: <详见 https://www.jianshu.com/p/9352c68a0635>
let fileName;
try {
const disposition = response.headers["content-disposition"];
fileName = decodeURIComponent(disposition.split("fileName=")[1]); // 中文需要转码 (前端乱码为百分号形式)
} catch (error) {
fileName = "导出文件";
}
if (!fileName.includes(".xls")) fileName += ".xls";
return { data: formatResponse(), fileName }; // 格式化输出
}
}
const msg = response.data.msg || response.data.message;
const code = Number(response.data.code);
if (code === CODE_SUCCESS) {
return formatResponse(() => {
// 需要show成功Message信息
if (response.config.conf[KEY_SHOW_MESSAGE])
progress.showSuccessMessage(msg);
});
}
// 请求报错: 显示成功的 Toast 提示
if (code === CODE_FINALLY) {
return Promise.reject(
formatResponse(() => progress.showSuccessMessage(msg))
);
}
// 登录失效跳转登录页面. NOTE: 注意可能导致循环调用
if (code === CODE_INVALID) {
progress.invalidToken(msg || TIP_INVALID_TOKEN);
}
// 请求结束loading和error处理
return _interceptorsLoadingAndMessage(
new Error(msg || TIP_ERROR_MESSAGE),
!response.config.conf[KEY_NO_ERROR_TIP]
);
},
error => {
return _interceptorsLoadingAndMessage(error);
}
);
2.2 将返回值格式为json,验证返回是否为流类型
// 将blob对象转化为json(文件类型调用ajax 取后端的返回值做特殊处理)
function _fileToJson(file) {
let data = {},
message = "";
function formatReturn() {
return { data, message };
}
return new Promise(resolve => {
const reader = new FileReader();
reader.onload = res => {
const { result } = res.target; // 得到字符串
try {
// 解析成json对象
data = JSON.parse(result);
} catch (err) {
message = err.message || err;
}
resolve(formatReturn());
}; // 成功回调
reader.onerror = err => {
message = err.message || err;
resolve(formatReturn());
}; // 失败回调
reader.readAsText(new Blob([file]), "utf-8"); // 按照utf-8编码解析
});
}
- 通用前端 blob 导出方式
/** 4.Excel:报表blob导出 */
request.exportExcel_blob = function(res = {}, fileName) {
const blob = new Blob([res.data], {
type: 'application/vnd.ms-excel;charset=UTF-8',
});
const link = document.createElement('a');
link.style.display = 'none';
link.href = URL.createObjectURL(blob);
link.download = fileName || res.fileName; // 下载后文件名: 拦截器处理 content-disposition, 传入优先
document.body.appendChild(link);
link.click();
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
};
- **至此就可以封装出通用的Excel接口
/** 5.Excel:报表blob导出 */
request.exportExcel = async function(url, params = {}, fileName) {
const res = await request.postData(url, params, {
[KEY_EXPORT_TYPE]: 'blob',
});
return this.exportExcel_blob(res, fileName);
};
2021.03.05 真是一个执着的人,为了这个问题研究了差不多1天,记录一下学习真好,开心 ~