如果想直接看怎么写的可以跳转到 解决方法 节!
需求描述
目前我们系统导出文件时,都是通过表单提交后,接收文件流自动下载。但由于在表单提交时没有相关调用前和调用后的回调函数,所以我们存在的问题,假如导出数据需要10秒,这期间前台依然可以操作,用户超过3秒没收到反馈会重复点击多次,导致后台查询压力过大卡死。因此要对功能做以下修改:
- 用户点击下载时弹出加载框提示
- 如果用户有相同条件的数据正在导出,需要弹出提示“文件正在下载,请稍后”(避免用户开了多个窗口点击)
系统现状
使用的技术框架:jQuery 1.11.3
(注意版本号
),EasyUI
(后端用的Struts2
,Spring
,Hibernate
,JDK8
)
前端请求下载的逻辑是通过iframe
跳转,接收到后端传回的二进制文件流,触发浏览器的自动下载来完成的。
前后端代码如下:
<html>
<body>
<form id="theForm2" name="theForm2" method="POST" enctype="multipart/form-data">
<div id="form-data-request-param" style="display: none;"></div>
</form>
<iframe id="oIframe" name="oIframe" frameborder="0" width="100%" height="100%" style="display: none;" src="<c:out value="${pageContext.request.contextPath}" />/pages/globals/blank.jsp"></iframe>
</body>
</html>
<script>
exportExcel: function() {
var requestParamForm = $('#form-data-request-param');
$('#form-data-request-param').empty();
let inputHiddenDataArr = [];
let rqParams = {};
rqParams['cond.beginDate'] = '2024-05-20'; // 入参1
rqParams['cond.endDate'] = '2024-05-21'; // 入参2
rqParams['cond.other'] = 'Y'; // 入参3
for (let rqName in rqParams) {
inputHiddenDataArr.push('<input type="hidden" name="' + rqName + '" value="' + rqParams[rqName] + '"/>');
}
$(inputHiddenDataArr.join('')).appendTo(requestParamForm);
let sTarget = 'oIframe';
let sFormName = 'theForm2';
let sUri = actionUri + '/exportExcel.shtml';
let form = document.forms[sFormName];
form.target = sTarget;
form.action = sUri;
// 无法监听到返回,所以也没有做加载框
form.submit();
}
</script>
@Controller("businessAction")
@Scope("prototype")
public class BusinessAction extends Struts2Action {
@Resource
private BusinessService service;
private Cond cond;
public String exportExcel() throws Exception {
// download方法的源码就不贴了, 内部逻辑是设置response的头信息Content-disposition=attachment; filename=xxx和Content-Type=application/octet-stream, 再通过输出流写出
FileUtils.download(ServletActionContext.getResponse(), this.service.export(this.getDownloadDir(), this.cond, this.getSessionBean()));
return null;
}
// 省略其他逻辑
}
public class BusinessServiceImpl extends BusinessService {
@ExportLog(serviceNode = "导出Excel")
public File export(String downloadDir, Cond cond, SessionBean sessionBean) throws Exception {
// ...省略查询等数据组装
File file = new File(downloadDir, "PC" + DateUtils.formatDate(new Date(), "yyyyMMddHHmmss") + ".xls");
return file;
}
}
处理思路及过程
- 需要添加和移除加载框,还有展示后端的错误信息,就得用ajax
- 后端返回的是文件流,需要确认jQuery的ajax是否支持下载文件流;如果不用文件流,服务器生成文件后返回下载链接到前台也行(但生成的文件在另外一个机器中,不在tomcat目录下,用户无法直接访问,所以还是采用返回文件流的方式)
- 不考虑异步导出,因为对于系统的改动比较大,需要引入延时框架或中间件,效益不高
因此决定后台依然返回文件流,前端用ajax请求,如果判断是文件流则下载,不是则弹出错误提示
过程
使用jQuery的$.ajax
一直都无法正常下载文件,后来查了一些文章表示jQuery的$.ajax
会把文件流的内容返回为字符串,需要生成Blob
对象后下载,使用以下两种写法,结果下载了打开文件会显示损坏
- 添加了
xhrFields: { responseType: 'blob' }
,jQuery3.x
可正常使用,1.11.x
版本使用报错:
Uncaught DOMException: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was 'blob').
注意
:换了3.0
版本后可以接收到blob对象,但项目中好多地方用到了jQuery,不敢轻易升级版本
- 将
dataType/responseType
设置为blob
也无效,依然接收到字符串类型,估计是$.ajax
将接收到的数据都先序列化成字符串了
折腾了好久,决定不用jQuery
的$.ajax
了,用原生的XMLHttpRequest
,查找它的写法来请求,结果终于正常接收到后端返回的Blob
对象了
解决方法
asyncDownloadFile: function(requestUrl, requestData, successCallback, beforeSendCallback, completeCallback, errorCallback) {
var formData = new FormData();
for (var key in requestData) {
formData.append(key, requestData[key]);
}
var xhr = new XMLHttpRequest();
xhr.open('POST', requestUrl, true);
//定义responseType='blob', 是读取文件成功的关键,这样设置可以解决下载文件乱码的问题
xhr.responseType = "blob";
xhr.onload = function() {
var data = this.response;
// 如果不是流信息, 说明有报错
if (response.type.indexOf('text/plain') >= 0) {
showMessage(data);
}
// 非文本内容, 后台返回了文件流, 在此处理
var disposition = decodeURI(xhr.getResponseHeader("Content-Disposition"))
,mimeType=xhr.getResponseHeader("Content-Type")
//通过Content-Type获取后端的文件名
var filename= getFilenameFromDisposition(disposition);
saveAsFile(data, filename, mimeType);
};
xhr.onerror = function() {
if (typeof errorCallback == 'function') {
errorCallback();
}
$.messager.alert('提示', '下载失败, 请联系管理员');
};
xhr.onloadend = function() {
$.messager.progress('close');
if (typeof completeCallback == 'function') {
completeCallback();
}
};
xhr.send(formData);
},
/** 解析文本内容*/
showMessage: function(data) {
var reader= new FileReader();
reader.readAsText(data,'UTF-8');
reader.onload = function() {
var res = JSON.parse(reader.result);
$.messager.alert('提示', res.ajaxError ? res.ajaxError : "服务器异常, 请联系管理员");
}
},
/** 通过disposition获取文件流的文件名 */
getFilenameFromDisposition: function (disposition){
var filename='';
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
return filename;
},
/** 保存文件到本地 */
saveAsFile: function (data, filename, mimeType) {
//兼容ie
if ('msSaveOrOpenBlob' in navigator) {
var blob = new Blob([data], { type: mimeType });
window.navigator.msSaveOrOpenBlob(blob, filename);
} else {
var blob = new Blob([data], { type: mimeType });
var url = window.URL.createObjectURL(blob);
var link = document.createElement('a');
document.body.appendChild(link);
link.style.display = 'none';
link.download = filename;
link.href = url;
link.click();
window.URL.revokeObjectURL(url);//手动释放blobURL,避免内存溢出
document.body.removeChild(link);
}
}
jQuery3.x的写法
$.ajax({
type: 'POST',
url: '请求地址',
xhrFields: {
responseType: 'blob'
},
data: requestData
success: function(response,status,xhr) {
if (response.type.indexOf('text/plain') >= 0) {
showMessage(response);// 复用上面代码块的方法
return;
}
// 复用上面代码块的方法
var fileName = getFilenameFromDisposition(xhr.getResponseHeader('Content-Disposition')); // 设置下载的文件名
saveAsFile(response, fileName, xhr.getResponseHeader('Content-Type'));
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error downloading file:', textStatus, errorThrown);
}
});
总结及反思
- 留意版本问题:在这个需求上耗费的时间主要集中在使用了不同版本的写法,结果大家都忽略了标注上自己的
jQuery
版本,导致相同的用法在低版本下无效 - 后台返回指定内容类型:后台注意区分返回文件流和文本的头信息
contentType
的返回,在我们系统会通过Struts
的拦截器类将异常信息使用contentType=text/plain
(文件流用的application/octet-stream
)写到response
的头信息中
参考链接
Ajax处理文件流下载
使用XMLHttpRequest处理文件流下载