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:;
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:;
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);
}
效果


formData批量上传的多种实现的更多相关文章
- 基于“formData批量上传的多种实现” 的多图片预览、上传的多种实现
前言 图片上传是web项目常见的需求,我基于之前的博客的代码(请戳:formData批量上传的多种实现)里的第三种方法实现多图片的预览.上传,并且支持三种方式添加图片到上传列表:选择图片.复制粘贴图片 ...
- asp.net+swfupload 多图片批量上传(附源码下载)
asp.net的文件上传都是单个文件上传方式,无法执行一次性多张图片批量上传操作,要实现多图片批量上传需要借助于flash,通过flash选取多个图片(文件),然后再通过后端服务进行上传操作. 本次教 ...
- 带进度条的文件批量上传插件uploadify
有时项目中需要一个文件批量上传功能时,个人认为uploadify是快速简便的解决方案. 先上效果图: 一. 下载uploadify 从官网下载uploadify的Flash版本(Flash版本免费,另 ...
- Mvc利用淘宝Kissy uploader实现图片批量上传附带瀑布流的照片墙
前言 KISSY 是由阿里集团前端工程师们发起创建的一个开源 JS 框架.它具备模块化.高扩展性.组件齐全,接口一致.自主开发.适合多种应用场景等特性.本人在一次项目中层使用这个uploader组件. ...
- 利用uploadify+asp.net 实现大文件批量上传。
前言 现在网上文件上传组件随便一搜都是一大堆,不过看大家一般都在用uploadify这个来上传文件.由于项目需要,我在来试了一下.因为第一次使用,也遇到了很多问题,特此记录! ------------ ...
- uploadify文件批量上传
uploadify能够时间文件的批量上传,JS文件包下载地址,使用说明可以参考官网文档(http://www.uploadify.com/documentation/) 使用方法如下代码: $(&qu ...
- WEB版一次选择多个文件进行批量上传(Plupload)的解决方案
WEB版一次选择多个文件进行批量上传(Plupload)的解决方案 转载自http://www.cnblogs.com/chillsrc/archive/2013/01/30/2883648.htm ...
- Mvc Kissy uploader实现图片批量上传 附带瀑布流的照片墙
前言 KISSY 是由阿里集团前端工程师们发起创建的一个开源 JS 框架.它具备模块化.高扩展性.组件齐全,接口一致.自主开发.适合多种应用场景等特性.本人在一次项目中层使用这个uploader组件. ...
- Springmvc+uploadify实现文件带进度条批量上传
网上看了很多关于文件上传的帖子,众口不一,感觉有点乱,最近正好公司的项目里用到JQuery的uploadify控件做文件上传,所以整理下头绪,搞篇文档出来,供亲们分享. Uploadify控件的主要优 ...
随机推荐
- 如何解决PeopleSoft Process Scheduler发布问题
常见PeopleSoft进程调度程序发布问题 此发布问题中遇到的一些常见错误是: 将HTTP响应转换为UCS2时出错 XML文档对象创建失败. 无法处理来自Report Repository的HTTP ...
- java points[复习]
1 - & 与 && 的区别: &:不管左边是true还是false,右端都会进行运算: &&:当左端为false时,右端不再进行运算: 即在与运算时, ...
- Conda命令指标
一.Conda相关指令 # 查看当前环境下已安装的包 conda list # 查看某个指定环境的已安装包 conda list -n tensorflow # 查找package信息 conda s ...
- Java中不定项参数(可变参数)的使用
Java1.5增加了新特性:可变参数:适用于参数个数不确定,类型确定的情况,java把可变参数当做数组处理. 注意事项: 1)不定项参数必须放在参数列表最后一个. 2)不定项参数只能有一个(多 ...
- .NET Standard 2.0正式发布了
亦可赛艇 前天(2017年8月14日),.NET Standard 2.0正式版终于发布了,与之相配套的.NET Core 2.0也同时正式发布,真是令人振奋. 详情请看:https://blogs. ...
- 基础:新建个maven项目
首先在电脑上配置好Maven环境 第一步:在Eclipse中选择创建Maven Project Next Next Finish 创建好后项目结构如下: 第二步:讲项目转为Web项目,右键项目点击pr ...
- [Swift]LeetCode159.具有最多两个不同字符的最长子串 $ Longest Substring with At Most Two Distinct Characters
Given a string S, find the length of the longest substring T that contains at most two distinct char ...
- [Swift]LeetCode761. 特殊的二进制序列 | Special Binary String
Special binary strings are binary strings with the following two properties: The number of 0's is eq ...
- mysql如何卸载完全
如何彻底的删除MySQL数据库: 以下操作以Window7操作系统为例: 1)停止window的MySQL服务. 找到“控制面板”-> “管理工具”-> “服务”,停止MySQL后台服务. ...
- innerHTML,outerHTML,innerText,outerText
- innerHTML 设置或获取位于对象起始和结束标签内的 HTML - outerHTML 设置或获取对象及其内容的 HTML 形式 - innerText 设置或获取位于对象起始和结束标签内的文 ...