前言
最近項目需要批量上傳附件,查了下資料,網上很多但看着一臉懵,只貼部分代碼,介紹也不詳細,這里記錄一下自己的采坑與多種實現,以免以后忘記。
這里先介紹下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);
}
效果



