文件流下载优化:由表单提交方式修改为Ajax请求

news2024/11/25 8:20:39

如果想直接看怎么写的可以跳转到 解决方法 节!

需求描述

目前我们系统导出文件时,都是通过表单提交后,接收文件流自动下载。但由于在表单提交时没有相关调用前和调用后的回调函数,所以我们存在的问题,假如导出数据需要10秒,这期间前台依然可以操作,用户超过3秒没收到反馈会重复点击多次,导致后台查询压力过大卡死。因此要对功能做以下修改:

  1. 用户点击下载时弹出加载框提示
  2. 如果用户有相同条件的数据正在导出,需要弹出提示“文件正在下载,请稍后”(避免用户开了多个窗口点击)

系统现状

使用的技术框架:jQuery 1.11.3(注意版本号),EasyUI(后端用的Struts2SpringHibernateJDK8
前端请求下载的逻辑是通过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;
    }    
}

处理思路及过程

  1. 需要添加和移除加载框,还有展示后端的错误信息,就得用ajax
  2. 后端返回的是文件流,需要确认jQuery的ajax是否支持下载文件流;如果不用文件流,服务器生成文件后返回下载链接到前台也行(但生成的文件在另外一个机器中,不在tomcat目录下,用户无法直接访问,所以还是采用返回文件流的方式)
  3. 不考虑异步导出,因为对于系统的改动比较大,需要引入延时框架或中间件,效益不高
    因此决定后台依然返回文件流,前端用ajax请求,如果判断是文件流则下载,不是则弹出错误提示

过程

使用jQuery的$.ajax一直都无法正常下载文件,后来查了一些文章表示jQuery的$.ajax会把文件流的内容返回为字符串,需要生成Blob对象后下载,使用以下两种写法,结果下载了打开文件会显示损坏

  1. 添加了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,不敢轻易升级版本

  1. dataType/responseType设置为blob也无效,依然接收到字符串类型,估计是$.ajax将接收到的数据都先序列化成字符串了

折腾了好久,决定不用jQuery$.ajax了,用原生的XMLHttpRequest,查找它的写法来请求,结果终于正常接收到后端返回的Blob对象了
接收到后台返回的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);
    }
});

总结及反思

  1. 留意版本问题:在这个需求上耗费的时间主要集中在使用了不同版本的写法,结果大家都忽略了标注上自己的jQuery版本,导致相同的用法在低版本下无效
  2. 后台返回指定内容类型:后台注意区分返回文件流文本的头信息contentType的返回,在我们系统会通过Struts的拦截器类将异常信息使用contentType=text/plain(文件流用的application/octet-stream)写到response的头信息中

参考链接

Ajax处理文件流下载
使用XMLHttpRequest处理文件流下载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1689362.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据分析】Numpy和Pandas库基本用法及实例--基于Japyter notebook实现

各位大佬好 &#xff0c;这里是阿川的博客 &#xff0c; 祝您变得更强 个人主页&#xff1a;在线OJ的阿川 大佬的支持和鼓励&#xff0c;将是我成长路上最大的动力 阿川水平有限&#xff0c;如有错误&#xff0c;欢迎大佬指正 承接上篇的博客 数据分析—技术栈和开发环境搭…

【Django】从零开始学Django(持续更新中)

PyCharm的版本必须为专业版&#xff0c;社区版不具备Web开发功能的。 一. Django建站基础 Django采用MTV的框架模式&#xff0c;即模型(Model)、模板(Template)和视图(Views)&#xff0c;三者之间各自负责不同的职责。 ●模型&#xff1a;数据存取层&#xff0c;处理与数据相关…

Macos14.4 安装MySQL5.7

文章目录 前言一、MySQL介绍二、安装步骤1.下载2.安装3.配置1.进入系统设置2.启动服务3.配置环境变量4.修改密码 FAQ1.双击安装时提示&#xff1a;检测恶意软件&#xff0c;无法打开2.修改环境变量文件提示&#xff1a;readonly option is set (add ! to override)文件权限不足…

go 微服务框架 kratos 日志库使用方法及原理探究

一、Kratos 日志设计理念 kratos 日志库相关的官方文档&#xff1a;日志 | Kratos Kratos的日志库主要有如下特性&#xff1a; Logger用于对接各种日志库或日志平台&#xff0c;可以用现成的或者自己实现Helper是在您的项目代码中实际需要调用的&#xff0c;用于在业务代码里…

学习图形推理

学习图形推理 1.位置规律1.1平移1.2翻转、旋转2.样式规律2.1加减异同2.2黑白运算3.属性规律3.1对称性3.2曲直性3.3开闭性4.数量规律4.1面4.2线数量4.3笔画数4.4点数量4.5素数量5.空间重构5.1相对面5.2相邻面-公共边5.3相邻面-公共点5.4相邻面-画边法题型 一组图:从左往右找规律…

SQL——SELECT相关的题目

目录 197、上升的温度 577、员工奖金 586、订单最多的客户 596、超过5名学生的课 610、判断三角形 620、有趣的电影 181、超过经理收入的员工 1179、重新格式化部门表 1280、学生参加各科测试的次数 1068、产品销售分析I 1075、项目员工I 1084、销售分析III 1327、列出指…

LLM-Llama在 MAC M1上体验Llama.cpp和通义千问Qwen 1.5-7B

Llama.cpp的主要目标是在各种硬件上&#xff08;本地和云端&#xff09;实现LLM推断&#xff0c;同时保持最小的设置和最先进的性能。 纯C/C实现&#xff0c;没有任何依赖关系Apple芯片是一级的支持对象 - 通过ARM NEON、Accelerate和Metal框架进行优化对x86架构的AVX、AVX2和…

后端之路第二站(正片)——SprintBoot之:分层解耦

很抽象&#xff0c;我自己也不好理解&#xff0c;仅作为一个前端转后端的个人理解 一、先解释一个案例&#xff0c;以这个案例来分析“三层架构” 这里我先解释一下黑马程序员里的这个案例&#xff0c;兄弟们看视频的可以跳过这节课&#xff1a;Day05-08. 请求响应-响应-案例_…

U-Mail邮件系统反垃圾解决方案,彻底解决垃圾邮件

随着互联网的普及和电子邮件的广泛应用&#xff0c;垃圾邮件已成为一种严重的网络污染。首先&#xff0c;垃圾邮件占用了大量的网络带宽&#xff0c;导致正常邮件的传输受阻&#xff0c;严重影响了用户的使用体验。其次&#xff0c;垃圾邮件中的恶意链接和欺诈信息可能导致用户…

day34 贪心算法 455.分发饼干 376. 摆动序列

贪心算法理论基础 贪心的本质是选择每一阶段的局部最优&#xff0c;从而达到全局最优。 贪心一般解题步骤&#xff08;贪心无套路&#xff09;&#xff1a; 将问题分解为若干个子问题找出适合的贪心策略求解每一个子问题的最优解将局部最优解堆叠成全局最优解 455.分发饼干 …

go routing 之 gorilla/mux

1. 背景 继续学习 go 2. 关于 routing 的学习 上一篇 go 用的库是&#xff1a;net/http &#xff0c;这次我们使用官方的库 github.com/gorilla/mux 来实现 routing。 3. demo示例 package mainimport ("fmt""net/http""github.com/gorilla/mux&…

Python知识详解【1】~{正则表达式}

正则表达式是一种用于匹配字符串模式的文本工具&#xff0c;它由一系列普通字符和特殊字符组成&#xff0c;可以非常灵活地描述和处理字符串。以下是正则表达式的一些基本组成部分及其功能&#xff1a; 普通字符&#xff1a;大多数字母和数字在正则表达式中表示它们自己。例如…

深度学习之基于MTCNN+Facenet的人脸识别身份认证系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 随着信息技术的快速发展&#xff0c;身份认证技术在日常生活和工作中的重要性日益凸显。传统的…

初始MyBatis ,详细步骤运行第一个MyBatis程序,同时对应步骤MyBatis底层剖析

1. 初始MyBatis &#xff0c;详细步骤运行第一个MyBatis程序&#xff0c;同时对应步骤MyBatis底层剖析 文章目录 1. 初始MyBatis &#xff0c;详细步骤运行第一个MyBatis程序&#xff0c;同时对应步骤MyBatis底层剖析每博一文案2. 前沿知识2.1 框架&#xff08;framework&#…

Oracle递归查询笔记

目录 一、创建表结构和插入数据 二、查询所有子节点 三、查询所有父节点 四、查询指定节点的根节点 五、查询指定节点的递归路径 六、递归子类 七、递归父类 一、创建表结构和插入数据 CREATE TABLE "REGION" ( "ID" VARCHAR2(36) DEFAULT SYS_GUI…

jdk17安装教程详细(jdk17安装超详细图文)

2021年9月14日JDK17 发布&#xff0c;其中不仅包含很多新语言功能&#xff0c;而且与旧版 JDK 相比&#xff0c;性能提升也非常明显。与之前 LTS 版本的 JDK 8 和 JDK 11 相比&#xff0c;JDK17 的性能提升尤为明显&#xff0c;本文将教你如何安装 相比于JDK1.8&#xff0c;JD…

信号:MSK调制和GMSK调制

目录 一、MSK信号 1. MSK信号的第k个码元 2.MSK信号的频率间隔 3.MSK信号的相位连续性 3.1 相位路径 3.2初始相位ψk 4.MSK信号的产生 原理框图 5.MSK信号的频谱图 二、高斯最小频移键控(GMSK) 1.频率响应 2.GMSK调制产生方式 2.1 高斯滤波器法 2.2 正交调制器法…

《MySQL怎样运行的》—InnoDB数据页结构

在上一篇文章中我们讲了&#xff0c;InnoDB的数据页是InnoDB管理存储空间的基本单位&#xff0c;一个页的大小基本为16kb 那你有没有疑问&#xff0c;就是说这个InnoDB的数据页的结构是什么样的&#xff0c;还有他这些结构分别有那些功能~接下来我们一一讲解 数据页的总览结构…

内部类知识点

什么是内部类&#xff1f; 内部类何时出现&#xff1f;B类是A类的一部分&#xff0c;且B单独存在无意义 内部类分类 成员内部类&#xff1a; 当内部类被private修饰后&#xff0c;不能用方法2 调用外部类成员变量 内部类里面有隐藏的outer this来记录 静态内部类 创建对象&…

路由引入实验(华为)

思科设备参考&#xff1a;路由引入实验&#xff08;思科&#xff09; 技术简介 路由引入技术在网络通信中起着重要的作用&#xff0c;能够实现不同路由协议之间的路由传递&#xff0c;并在路由引入时部署路由控制&#xff0c;实现路径或策略的控制 实验目的 不同的路由协议之…