文件流下载优化:由表单提交方式修改为Ajax请求
如果想直接看怎么写的可以跳转到 解决方法 节!
需求描述
目前我们系统导出文件时,都是通过表单提交后,接收文件流自动下载。但由于在表单提交时没有相关调用前和调用后的回调函数,所以我们存在的问题,假如导出数据需要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'; // 入参1rqParams['cond.endDate'] = '2024-05-21'; // 入参2rqParams['cond.other'] = 'Y'; // 入参3for (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 {@Resourceprivate 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) {//兼容ieif ('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: requestDatasuccess: 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处理文件流下载
相关文章:
文件流下载优化:由表单提交方式修改为Ajax请求
如果想直接看怎么写的可以跳转到 解决方法 节! 需求描述 目前我们系统导出文件时,都是通过表单提交后,接收文件流自动下载。但由于在表单提交时没有相关调用前和调用后的回调函数,所以我们存在的问题,假如导出数据需…...
基础3 探索JAVA图形编程桌面:逻辑图形组件实现
在一个宽敞明亮的培训教室里,阳光透过窗户柔和地洒在地上,教室里摆放着整齐的桌椅。卧龙站在讲台上,面带微笑,手里拿着激光笔,他的眼神中充满了热情和期待。他的声音清晰而洪亮,传遍了整个教室:…...
前后端部署笔记
windows版: 如果傻呗公司让用win电脑部署,类似于我们使用笔记本做局域网服务器,社内使用。 1.安装win版的nginx、mysql、node、jdk等 2.nginx开机自启参考Nginx配置及开机自启动(Windows环境)_nginx开机自启动 wind…...
设计模式9——适配器模式
写文章的初心主要是用来帮助自己快速的回忆这个模式该怎么用,主要是下面的UML图可以起到大作用,在你学习过一遍以后可能会遗忘,忘记了不要紧,只要看一眼UML图就能想起来了。同时也请大家多多指教。 适配器模式(Adapte…...
一文了解基于ITIL的运维管理体系框架
本文来自腾讯蓝鲸智云社区用户:CanWay ITIL(Information Technology Infrastructure Library)是全球最广泛使用的 IT 服务管理方法,旨在帮助组织充分利用其技术基础设施和云服务来实现增长和转型。优化IT运维,作为企业…...
Web前端开发技术-格式化文本 Web页面初步设计
目录 Web页面初步设计 标题字标记 基本语法: 语法说明: 添加空格与特殊符号 基本语法: 语法说明: 特殊字符对应的代码: 代码解释: 格式化文本标记 文本修饰标记 计算机输出标记 字体font标记 基本语法: 属…...
Windows下部署Seata1.5.2,解决Seata无法启动问题
目录 1. 版本说明 2. Windows下部署Seata1.5.2 2.1 创建回滚日志表undo_log 2.2 创建Seata服务端需要的四张表 2.3 在nacos创建seata命名空间,添加seataServer.yml配置 2.4 修改本地D:/tool/seata-server-1.5.2/seata/conf/applicaltion.yml文件 2.5 启动Seat…...
我加入了C++交流社区
最近,我决定加入了一个C交流社区,这是一个专注于C编程语言的在线平台。加入这个社区的初衷是为了提升我的编程技能,与其他对C感兴趣的人交流经验和知识。 加入这个社区后,我发现了许多有趣的讨论和资源。每天都有各种各样的话题&…...
Vue从入门到实战Day11
一、为什么要学Vue3 Vue3官网:简介 | Vue.js 1. Vue3的优势 2. Vue2选项式API vs Vue3组合式API 示例: 二、create-vue搭建Vue3项目 1. 认识create-vue create-vue是Vue官方新的脚手架工具,底层切换到了vite(下一代构建工具),为…...
day15|各种遍历的应用
相关题目: 层次遍历会一打十 反转二叉树 对称二叉树 层次遍历会一打十 自底向上的层序遍历 实现思路:层次遍历二叉树,将遍历后的结果revers即可 public List<List<Integer>> levelOrderBottom(TreeNode root) {List<List&l…...
第12周作业--HLS入门
目录 一、HLS入门 二、HLS入门程序编程 创建项目 1、点击Vivado HLS 中的Create New Project 2、设置项目名 3、加入文件 4、仿真 3、综合 一、HLS入门 1. HLS是什么?与VHDL/Verilog编程技术有什么关系? HLS(High-Level Synthesis,…...
WorkManager使用技巧及各Android版本适配
WorkManager使用技巧及各Android版本适配 WorkManager是Android Jetpack中用于处理异步任务的库,它能够保证任务即使在应用关闭或设备重启后也能被执行。以下是WorkManager的使用技巧和代码示例,以及不同Android版本的适配方法。 1. 初始化WorkManager…...
鼠标滚轮使用时上下跳动的解决方法
前阵子鼠标滚轮使用时总会出现上下跳动比如向下滚动会往上反弹或者是在当前框架卡住但颤动的情况,这个问题困扰了我很久,试过了很多设置和驱动方面的办法都没解决,因此大概率是滚轮那有脏东西了。最后终于在一个答复下面看到了一种不用拆开修…...
CSS【常用CSS样式、盒子模型、定位、浮动 、扩展样式】--学习JavaEE的day46
day46 CSS 练习 页面实现: 分析: 未优化: 优化: 参考代码:(包含样式优化–选择器CSS属性) 先写上table方便实现,之后再去除即可 name没有服务器,可暂时不写 <!…...
os.path 提供用于处理文件路径和文件的系统函数
在Python中,os.path模块提供了一系列用于处理文件路径和文件的系统函数。 获取文件路径信息 os.path.abspath(): 获取文件的绝对路径。os.path.dirname(): 获取文件路径的目录名。os.path.basename(): 获取文件路径的文件名。os.path.split(): 分割路径为目录和文件…...
golang通过go-aci适配神通数据库
1. go-aci简介 go-aci是神通数据库基于ACI(兼容Oracle的OCI)开发的go语言开发接口,因此运行时需要依赖ACI驱动和ACI库的头文件。支持各种数据类型的读写、支持参数绑定、支持游标范围等操作。 2. Linux部署步骤 2.1. Go安装: 版本:1.9以上…...
【Vue】Vue2中的Vuex
目录 Vuex介绍Vuex 中的核心概念 在vue2中使用Vuex安装 Vuex创建一个 Vuex Store在 Vue 实例中使用 Vuex编写 Vuex 的 state、mutations 和 actions在组件中使用 Vuex Vuex的核心State组件中获取 Vuex 的状态mapState 辅助函数对象展开运算符 Getter基本使用示例 通过属性访问通…...
前端生成二维码
直接img标签显示 npm i use_qrcode npm包地址 <img :src"qrcode" alt"QR Code" /> const txt: any ref(https://baidu.com) const qrcode useQRCode(txt) const qrcodeLogo useQRCode(txt, { logoSrc: https://www.antdv.com/assets/logo.1ef800…...
wordpress woocommer 添加代码实现,点击按钮,将产品添加到购物车并且跳转到结账页面
wordpress woocommer 添加代码实现,点击按钮,将产品添加到购物车并且跳转到结账页面 案列代码1,解决的是普通产品的 //短代码生成按钮,传入短代码,点击直接到达结账页面 function add_product_to_cart_button($atts)…...
Scala学习笔记6: 类
目录 第六章 类1- 简单类和无参方法2- 带有getter和setter的属性3- 只带getter的属性4- 对象私有化5- 辅助构造器6- 主构造器7- 嵌套类end 第六章 类 在Scala中, 类用于创建对象的蓝图; 类可以包含方法、值、变量、类型、对象和特质等成员; 类名应该以大写字母开头, 可以包含…...
观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
在rocky linux 9.5上在线安装 docker
前面是指南,后面是日志 sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io -y docker version sudo systemctl start docker sudo systemctl status docker …...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南
🚀 C extern 关键字深度解析:跨文件编程的终极指南 📅 更新时间:2025年6月5日 🏷️ 标签:C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言🔥一、extern 是什么?&…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...
听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
