formData批量上传的多种实现
前言
最近项目需要批量上传附件,查了下资料,网上很多但看着一脸懵,只贴部分代码,介绍也不详细,这里记录一下自己的采坑与多种实现,以免以后忘记。
这里先介绍下FormData对象,以下内容摘自:https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
XMLHttpRequest Level 2添加了一个新的接口FormData
.利用FormData对象
,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()
方法来异步的提交这个”表单”.比起普通的ajax,使用FormData
的最大优点就是我们可以异步上传一个二进制文件.
在我的自定义input文件上传样式里就已经实现里单文件上传,并且实现了自定义input样式;如果构造FormData对象是传入表单js对象,formData会自动注入表单里的值;如果是new一个空对象,然后手动append的表单类型为file时要注意:这里append进去的是File对象,而不是FileList对象
效果
先看一下大概效果:
代码编写
controller有两种方法:三种方式调的都是用一个接口
/** * 批量上传 */ @PostMapping("upload") public ResultModel<List<AttachmentVo>> upload(HttpServletRequest request, @RequestParam("applyId") String applyId){ List<MultipartFile> multipartFileList = ((MultipartHttpServletRequest) request).getFiles("attachment"); System.out.println(multipartFileList.size()); System.out.println(applyId); return null; } /** * 批量上传2 (推荐使用) */ @PostMapping("upload2") public ResultModel<List<AttachmentVo>> upload2(MultipartFile[] attachment,@RequestParam("applyId") String applyId){ System.out.println(attachment.length); System.out.println(applyId); return null; }
自定义样式:(三种方式都是用这个样式),要引入bootstrap, 图标用的是font awesome
.nav-bar { border-top: 1px solid #9E9E9E; margin: 10px 0 20px; } .nav-bar-title { margin: -13px 0 0 35px; background-color: white; padding: 0 10px; float: left; color: #199ED8; } .attachment-remove { font-size: 25px; color: red; margin-left: 5px; cursor: pointer; } .attachment-text-p { border: 1px solid #c2cad8; padding: 5px 5px; margin: 0; float: left; height: 30px; width: 90%; } .attachment-text-p + i { float: left; line-height: 30px !important; } .input-attachment { width: 90% !important; padding: 4px 12px !important; }
方式1
点击Add,追加一个input,点击Delete,删除一个input,点击叉号也可以删除对应的input,需要单独为每个input选择文件
效果
html
<form id="attachments" enctype="multipart/form-data" class="form-horizontal nice-validator n-yellow" novalidate="novalidate"> <div class=\'form-body\'> <div class=\'form-group\'> <label class="control-label col-md-1">附件管理:</label> <div class="col-md-4"> <button id="attachmentAddBtn" type="button" class="btn btn-default">Add Attachment</button> <button id="attachmentDeleteBtn" type="button" class="btn btn-default">Delete Attachment</button> <button id="attachmentUploadBtn" type="button" class="btn btn-default">Upload</button> </div> </div> <div class=\'form-group\'> <label class="control-label col-md-1">附件上传:</label> <div id="attachmentInputs" class="col-md-3"> </div> </div> </div> </form>
js
//attachment-remove $("#attachmentInputs").on("click", ".attachment-remove", function (even) { $(this).prev().remove();//删除上一个兄弟节点 $(this).remove();//删除自己 }); //add but $("#attachmentAddBtn").click(function (even) { //name值一样就可以 $("#attachmentInputs").append("<input name=\"attachment\" type=\"file\" class=\"form-control input-attachment\"/><i class=\"fa fa-times attachment-remove\"></i>"); }); //delete $("#attachmentDeleteBtn").click(function (even) { var files = $("#attachmentInputs input[type=\'file\']"); files.each(function (index, element) { //从最下面开始删除,至少保留一个 if (!(index === 0) && index === (files.length - 1)) { $(element).next().remove(); $(element).remove(); } }); }); //upload $("#attachmentUploadBtn").click(function (even) { //1、通过HTML表单创建FormData对象 自动注入 // var formData = new FormData($("#attachments")[0]); //2、从零开始创建FormData对象 手动注入 var formData = new FormData(); //注入 name=file var files = $("#attachmentInputs input[type=\'file\']"); for (var i = 0; i < files.length; i++) { //注意:这里append进去的是File对象,而不是FileList对象 formData.append("attachment", files[i].files[0]); } //注入name=text formData.append("applyId", "123456"); console.log(formData.getAll("attachment")); //执行上传 $.ajax({ url: ctx + "/attachment/upload2", type: "post", data: formData, processData: false, contentType: false, success: function (data) { }, error: function (e) { } }); }); //add one input $("#attachmentAddBtn").click();
方式2
第二种方式只有一个input,用的是multiple=”multiple”属性,可以再弹窗里选择多个文件提交,如果再加工一下,也做成第三种一样,展示出文件名,同时可以删除对应的文件
效果
html
<form id="attachments2" enctype="multipart/form-data" class="form-horizontal" novalidate="novalidate"> <div class=\'form-body\'> <div class=\'form-group\'> <label class="control-label col-md-1">附件管理:</label> <div class="col-md-4"> <button id="attachmentUploadBtn2" type="button" class="btn btn-default">Upload</button> </div> </div> <div class=\'form-group\'> <label class="control-label col-md-1">附件上传:</label> <div id="attachmentInputs2" class="col-md-3"> <input name="attachment" type="file" class="form-control input-attachment" multiple="multiple"/> </div> </div> </div> </form>
js
//upload2 $("#attachmentUploadBtn2").click(function (even) { //1、通过HTML表单创建FormData对象 自动注入 // var formData = new FormData($("#attachments2")[0]); //2、从零开始创建FormData对象 手动注入 var formData = new FormData(); //注入 name=file var files = $("#attachmentInputs2 input[type=\'file\']"); for (var i = 0; i < files[0].files.length; i++) { formData.append("attachment", files[0].files[i]); } //注入name=text formData.append("applyId", "123456"); console.log(formData.getAll("attachment")); //执行上传 $.ajax({ url: ctx + "/attachment/upload2", type: "post", data: formData, processData: false, contentType: false, success: function (data) { }, error: function (e) { } }); });
方式3
定义了一个隐藏的input,并将Select File按钮的click与input的click对等,点击按钮相当于点击input,弹出选择文件对话框,监听了input的change事件,将选择的file对象push到全局数组变量attachmentArray中,点击Upload时再遍历注入到formData中
效果
html
<form id="attachments3" enctype="multipart/form-data" class="form-horizontal" novalidate="novalidate"> <div class=\'form-body\'> <div class=\'form-group\'> <label class="control-label col-md-1">附件管理:</label> <div class="col-md-4"> <button id="selectFile" type="button" class="btn btn-default">Select File</button> <button id="attachmentUploadBtn3" type="button" class="btn btn-default">Upload</button> </div> </div> <div class=\'form-group\'> <label class="control-label col-md-1">附件上传:</label> <input id="attachmentInputs3" type="file" style="display: none;"/> <div id="attachmentText3" class="col-md-3"> </div> </div> </div> </form>
js
//存放file对象 var attachmentArray = []; //attachment-remove $("#attachmentText3").on("click", ".attachment-remove", function (even) { //删除attachmentArray数据 attachmentArray.splice($(this).data("index"), 1); //删除html对象 $(this).prev().prev().remove(); $(this).prev().remove(); $(this).remove(); }); //Select File $("#selectFile").click(function (even) { // 获取input $("#attachmentInputs3").click(); }); //input change $("#attachmentInputs3").change(function (even) { // 获取input var fileName = $(this).val(); var file = $(this)[0].files[0]; //是否选择了文件 if (fileName) { attachmentArray.push(file); $("#attachmentText3").append("<div><p class=\'attachment-text-p\'>" + fileName + "</p><i data-index=\'" + (attachmentArray.length - 1) + "\' class=\"fa fa-times attachment-remove\"></i></div>") } }); //upload3 $("#attachmentUploadBtn3").click(function (even) { //这里只能手动注入 var formData = new FormData(); //遍历数据,手动注入formData for (var i = 0; i < attachmentArray.length; i++) { formData.append("attachment", attachmentArray[i]); } formData.append("applyId", "123456"); console.log(formData.getAll("attachment")); //执行上传 $.ajax({ url: ctx + "/attachment/upload", type: "post", data: formData, processData: false, contentType: false, success: function (data) { }, error: function (e) { } }); });
2019-12-31更新:感谢 Spring2Sun 指出错误之处,发现了方式三的一个bug;
bug描述:进行删除后,数组长度已经减1,但标签的data-index的值没有更新,导致后面再进行删除时下标对应不上,删除后标签与数据对应错乱
bug修复:
1、在移除数组和元素后,对剩下的i 标签data-index 重置下顺序
//删除 $("#attachmentText3").on("click", ".attachment-remove", function (even) { //其他地方不变,省略代码... //重新排序标签data-index $("#attachmentText3 i").each(function (index, element) { $(element).attr("data-index", index); }); });
2、进行删除时不再删除数组数据,而是将对应的下标设置成“-1”,上传遍历数组时,再进行判断不等于”-1″时才append
//删除 $("#attachmentText3").on("click", ".attachment-remove", function (even) { //将值设置为"-1" attachmentArray[$(this).data("index")] = "-1"; //其他地方不变,省略代码... });
//上传 $("#attachmentUploadBtn3").click(function (even) { //这里只能手动注入 var formData = new FormData(); //遍历数据,手动注入formData for (var i = 0; i < attachmentArray.length; i++) { let value = attachmentArray[i]; if(value != "-1"){ formData.append("attachment", value); } } //其他地方不变,省略代码... });
后记
最后看一下file数据、请求头、还有振奋人心的后台成功接参图
file数据
请求头
成功接参
新需求
项目需要支持同一张单上面有多个上传组件,按照我们之前的三种方式并不满足,第一种使用了id的方式去绑定,当多个组件在同一个html的时候就不行了,第三种我们采用一个全局数组变量来存选中的file,但之前一个组件有引一次js,当多个的时候就会重复引入,后面引入的变量、方法就会覆盖前面,同时,应该用的是id,当我们调用upload方式时不知道applyId工单号对应的form是哪一个,无法绑定附件的工单号,这里改进一下,将第一种跟第三种整合一下。
上传组件html
使用的是thymeleaf,th:text=”#{attachment.title}”是国际化,<script th:replace=”common/head::static”></script>引入的是公用的js、css,上传组件的js、css写在common里面,所有的页面都会引入它们,而且只引入一次。这里给每个form表单绑定一个applyId属性,对应具体的工单号,这样我们调用upload的时候就可以找到对应的form表单
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title th:text="#{attachment.title}"></title> <script th:replace="common/head::static"></script> </head> <body> <!-- 使用方法:在任意工单页面添加此DIV <div th:replace="attachment/attachment::attachmentPage(${applyId})"></div> 调用上传方法:Attachment.upload(${applyId}); --> <div th:fragment="attachmentPage(applyId)"> <div class="nav-bar"><span class="nav-bar-title" th:text="#{attachment.title}"></span></div> <form th:applyId="${applyId}" class="form-horizontal attachments-form" enctype="multipart/form-data"> <div class=\'form-body\'> <div class=\'form-group\'> <label class="control-label col-md-1">附件管理:</label> <div class="col-md-4"> <button type="button" class="btn btn-default" onclick="Attachment.appendAttachmentInput(this)"> Select File </button> </div> </div> <div class=\'form-group\'> <label class="control-label col-md-1">附件列表:</label> <div class="col-md-10 attachments-list"></div> </div> </div> </form> </div> </body> </html>
其他任意html调用
thymeleaf的传值方式之一,与组件html的 th:fragment=”attachmentPage(applyId)” 配合使用,后面就可以这样使用 th:applyId=”${applyId}”
<div th:replace="attachment/attachment::attachmentPage(123456)"></div> <div th:replace="attachment/attachment::attachmentPage(111111)"></div>
common.js 上传组件部分
removeAttachmentInputListener,监听×号的点击事件,要在common.js执行一次。
/** * 三、附件上传的方法 */ var Attachment = { //上传附件 upload: function (applyId) { //终止上传 if (!applyId) { layer.msg(i18n(\'attachment.applyid.is.null\')); return; } //添加附件 var formData = new FormData(); $("form[applyId=\'"+applyId+"\']").find("input[name=\'attachment\']").each(function (index, element) { //过滤操作:input框有值,才append到formData if ($(element).val()) { formData.append("attachment",element.files[0]); } }); //追加applyId到formData formData.append("applyId", applyId); //执行上传 $.ajax({ url: ctx + "/attachment/upload", type: "post", data: formData, processData: false, contentType: false, success: function (data) { if (checkResult(data)) { console.log(\'附件上传成功:\', data); } else { throw e; } }, error: function (e) { console.log(\'附件上传失败\'); throw e; } }); }, //添加附件 appendAttachmentInput: function (btn) { //先追加html $(btn).parents(\'.attachments-form\').find(".attachments-list").append("<div><input type=\"file\" name=\"attachment\" class=\"hidden\"/></div>"); //最新追加的input var attachments = $(btn).parents(\'.attachments-form\').find(".attachments-list").find("input[name=\'attachment\']"); //绑定input的change事件,注意:当我们点击取消或×号时并不触发,但是无所谓,我们在upload方法进行过滤空的input就可以了 attachments[attachments.length - 1].onchange = function(){ var fileName = $(this).val(); if (fileName) { $(this).parent("div").append("<p class=\'attachment-text-p\'>" + fileName + "</p><i class=\"fa fa-times attachment-remove\"></i>"); }else{ $(this).parent("div").remove(); } }; //触发最新的input的click attachments[attachments.length - 1].click(); }, //删除附件 removeAttachmentInputListener: function () { $(".attachments-form").on("click", ".attachment-remove", function (even) { $(this).parent().remove(); }); } };
common.css 上传组件部分
.attachment-remove { font-size: 25px; color: red; margin-left: 5px; cursor: pointer; } .attachment-text-p { border: 1px solid #c2cad8; padding: 5px 5px; margin: 0; float: left; height: 30px; width: 90%; margin-top: 5px; } .attachment-text-p + i { float: left; line-height: 30px !important; margin-top: 5px; }
新需求效果
报错记录:org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field images exceeds its maximum permitted size of 1048576 bytes.
解决:调大http的最大上传大小
string: http: multipart: max-file-size: 5Mb #单个文件大小 max-request-size: 50Mb #总大小
string: servlet: multipart: max-file-size: 5Mb #单个文件大小 max-request-size: 50Mb #总大小
导出文件到浏览器
2019-10-24补充:上传、下载通常是密不可分的两个功能,这里记录一下如何导出文件到浏览器然后下载到本地
前端js
//数据数组,ids let data = [1,2,3,4]; //ajax不支持下载类型,使用location.href或者表单提交 //window.location.href,get提交,数据会暴露在URL,相对不安全 //创建临时的、隐藏的form表单,post提交,数据在请求体里,相对安全 var $form = $(document.createElement(\'form\')).css({display: \'none\'}).attr("method", "POST").attr("action", ctx + "/downLoad"); var $input = $(document.createElement(\'input\')).attr(\'name\', "ids").val(JSON.stringify(data)); $form.append($input); $("body").append($form); $form.submit(); //提交完成后remove掉 $form.remove();
java后端
@PostMapping("/downLoad") public ResponseEntity downLoad(String ids) throws IOException { //json字符串转换成对象 List<String> idList = new ObjectMapper().readValue(ids, TypeFactory.defaultInstance().constructCollectionType(List.class, String.class)); //处理、拼接数据 List<StringBuilder> students = new ArrayList<>(); assert idList != null; idList.forEach((id) -> { //账号-密码-区服-角色-道具 StringBuilder str = new StringBuilder(); SuperSearchUcidVo searchUcidVo = superSearchUcidService.get(Integer.valueOf(id)).getData(); str.append(searchUcidVo.getUserName()).append("-"); str.append(searchUcidVo.getPassword()).append("-"); str.append(searchUcidVo.getDivision()).append("-"); if(!StringUtils.isEmpty(searchUcidVo.getRoleList())){ String[] roleList = searchUcidVo.getRoleList().split(","); for (String role : roleList) { str.append(role).append("|"); } } str.append("-"); if(!StringUtils.isEmpty(searchUcidVo.getPropsList())){ String[] propsList = searchUcidVo.getPropsList().split(","); for (String props : propsList) { str.append(props).append("|"); } } students.add(str); }); StringBuilder write = new StringBuilder(); students.forEach((str) -> write.append(str).append("\n")); //文件数据、文件名 byte[] fileBytes = write.toString().getBytes("GBK"); String fileName = "GameAccountExport_" + new Date().getTime() + ".txt"; //设置响应头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); headers.setContentDispositionFormData("attachment", fileName); //下载文件 return new ResponseEntity<>(fileBytes, headers, HttpStatus.CREATED); }
效果