引言

星期四的时候,我遇到了一个文件上传的问题,与以往不同的是,这一次上传的是多个文件,而且涉及到了久违的javascript代码。
虽然最后实现的并不尽如人意,不过也算是完成了功能,接下来就把我发现的一些问题和总结记录一下。

HTML部分展示

<form id ="tempFile" method="post" enctype="multipart/form-data">
	<!-- 指令模板文件上传 -->
	<div style="text-align: center;">
		<div class="clearfix" style="width:400px;margin:0 auto;">
   			<input id ="templatesFile" type="file" multiple="multiple" />
   		</div>
  	</div>
</form>

上述代码可以看到,只是一个简单的 form表单,里面嵌套了一个 input 输入框,这个 input 输入框的 type 类型是file,值得注意的是,如果是允许上传多个文件,那么input输入框必须添加multiple="multiple" 属性
这里要说明一点,由于使用 submit 按钮提交 form表单会造成页面刷新,因此,现在一般都采用按钮与 form 表单分离的方式,使用 AJAX来异步上传数据。 如下所示:

<button class="btn btn-default"  onclick="sendTemplates();" title="upload">
上传模板文件
</button>

JS代码展示

/**
 * 上传模板文件
 * @returns
 */
function sendTemplates() {
	// 需要上传的文件
//	var templates = document.getElementById("templatesFile").files;
	var templates = $("#templatesFile")[0].files;
	if (templates.length > 2) {
		alert("上传失败,一次最多两个文件!");
		return;
	}
	var formdata = new FormData();
	for (var i = 0 ; i < templates.length ; i++) {
		formdata.append("temp" + i, templates[i]);
	}
	$.ajax({
		url : "/manager/uploadTemplates",
		type : "POST",
		data : formdata,
		processData : false,
		contentType : false,
		success : function(resultMap) {
			if (resultMap.code == "OK") {
				alert("上传成功!");
			} else if (resultMap.code == "FAIL") {
				alert(resultMap.msg);
			}
		},
		error : function(e) {
			alert("服务器异常");
		}
	});
}

代码已经保留了关键的代码部分,剔除了一些无关紧要的操作。

Java代码实现

@RequestMapping(value = "/uploadTemplates", method = RequestMethod.POST)
@ResponseBody
public Map<String, Object> uploadTemplates(HttpServletRequest request) {
	MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
	List<MultipartFile> templatesList = new ArrayList<>();
	for (int i = 0 ; i < 2 ; i++) {
		templatesList.addAll(multipartRequest.getFiles("temp" + i));
	}
	MultipartFile[] templates = new MultipartFile[templatesList.size()];
	templatesList.toArray(templates);
	Map<String, Object> result = templateService.uploadTemplates(templates);
	return result;
}

看到上面的代码,我不得不说,真的和 shi一样。不过目前我还没找到很好的办法。
好在 for 循环的 getFiles(...) 如果取不到值的话,就会返回一个空的 List 不会直接抛出异常。
以上就是所有关键的代码部分,下面来说一说调试过程中我使用过的几种方法,以及觉得很坑的地方。

第一坑:jQuery取files要加个[0]

回看js 部分的代码,相信你已经注意到了:

var templates = $("#templatesFile")[0].files;

这是通过jQuery选择器找到 id="templatesFile"input标签,并获取其中存储的 FileList

java js 上传excel文件 js formdata上传文件_java js 上传excel文件


FileList 并不是一个数组。如果希望将它转化成一个数组,可以使用 Array.from(FileList)

java js 上传excel文件 js formdata上传文件_多文件上传_02


不过不论是 FileList还是数组,都不是重点,重点是$("#templatesFile")[0] 一定要加上后面的这个 [0]而如果使用document.getElementById("...")就不必加 [0] 这是因为 files 属性是原生js属性,而不是jQuery属性,因此需要通过这个 [0]jQuery对象转化为 js对象,才能通过 .files 获得这个FileList

第二坑:$.ajax 的data域要怎么写以及controller如何接收

说实话,前面的 js 与 java部分的代码实在是迫不得已,不过依然没有找到恰当合适的方法。
这个问题困扰了我整整一天的时间,从星期四的下午开始研究一直到第二天星期五的上午才勉强以上面的代码跑通功能。 之前做过上传单个文件的功能。那天我乐观的以为,只要前端能够获取到 files 数组,然后后端的接口使用 MultipartFile[] 来接收就可以大功告成了!像这样:

@RequestMapping(value = "/uploadTemplates", method = RequestMethod.POST)
@ResponseBody
public String uploadTemplates(MultipartFile[] templates) {
   // ...some codes
}

但是前端的代码我试过N种方法依然无法成功接收到这个文件数组!不论是这样:

java js 上传excel文件 js formdata上传文件_java js 上传excel文件_03


还是这样:

java js 上传excel文件 js formdata上传文件_java js 上传excel文件_04


亦或是将数组转化成String,然后后台用String接收:

java js 上传excel文件 js formdata上传文件_上传_05


完!全!没!用!


明明以前传其他类型的参数是OK的啊!这到底是为什么?

于是看到了 FormData 的解决方案。但是事情依然并不简单。

一开始我的思路是封装一个FormData对象,然后把文件数组放入到一个value中,并且指定一个key ,这样后台通过 getFiles() 方法就可以获得整个文件列表了。但是并没有奏效。

在网上找了下原因,看到了一些别人的代码,**全都是每一个key放一个文件!**原因是:

java js 上传excel文件 js formdata上传文件_FormData_06



简单的说, StringBlobFile可以被FormData传输,如果是其他类型,则会被转化成字符串,然而,对于FileList又会是怎样的情况呢?我找到了下面这句话:

Using the FormData API is the simplest and fastest, but has the
disadvantage that data collected can not be stringified.

翻译过来就是,使用FormData最简单,也最高效,但是有一个缺点是,数据集合(比如List或者数组)无法被序列化为字符串。
我真的崩溃了!也就是说,我必须将文件列表(或者数组)中的每一个文件取出来,分别给每一个文件指定一个Key才能成功的通过formdata传输到后台并接收。
所以才有了这样的代码:

var formdata = new FormData();
for (var i = 0 ; i < templates.length ; i++) {
	formdata.append("temp" + i, templates[i]);
}

和这样的代码:

public Map<String, Object> uploadTemplates(HttpServletRequest request) {
	MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
	List<MultipartFile> templatesList = new ArrayList<>();
	for (int i = 0 ; i < 2 ; i++) {
		templatesList.addAll(multipartRequest.getFiles("temp" + i));
	}
	// some other codes...
}

到目前为止,还没有找到更加简洁和高效的代码,有哪位全栈的哥们可以告诉我吗?

欢迎评论区留言哦!非常感谢!