文件流下载优化:由表单提交方式修改为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中, 类用于创建对象的蓝图; 类可以包含方法、值、变量、类型、对象和特质等成员; 类名应该以大写字母开头, 可以包含…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
TDengine 快速体验(Docker 镜像方式)
简介 TDengine 可以通过安装包、Docker 镜像 及云服务快速体验 TDengine 的功能,本节首先介绍如何通过 Docker 快速体验 TDengine,然后介绍如何在 Docker 环境下体验 TDengine 的写入和查询功能。如果你不熟悉 Docker,请使用 安装包的方式快…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
