前端上传数据时,必定会用 FormData 进行封装。FormData 是一种 key-value 的集合。发送数据时要考虑 FormData 中的数据都是什么格式的数据,即设置 content-type。
这是一个表单的案例:
<form id="form" action="http://localhost:8080/upload/user/info" method="post"> <input id="uname" type="text" name="uname" /> <input id="pwd" type="password" name="pwd" /> <input type="submit" value="Submit!" /> </form>
用户只需要点击最后一个 input,就可以提交所有表单里面的数据到服务器。这里什么 JS 代码都没有写,到底是如何做到的呢?
查阅 MDN 文档,实际上前端发送数据使用的是 XMLHttpRequest。表单数据通过 XMLHttpRequest.send() 请求特定 URL。XMLHttpRequest 在 AJAX 编程中被大量使用。XMLHttpRequest.send() 函数可以发送的数据类型有以下几种:
XMLHttpRequest.send(); XMLHttpRequest.send(ArrayBuffer data); XMLHttpRequest.send(ArrayBufferView data); XMLHttpRequest.send(Blob data); XMLHttpRequest.send(Document data); XMLHttpRequest.send(DOMString? data); XMLHttpRequest.send(FormData data);
表单使用这个函数发送数据的,那又是哪一个参数类型的函数呢?其实就是最后一个函数。继续往下看,FormData 是什么?
FormData 是用来表示表单数据的键值对集合。表单其实就是一个 Map 集合,但是,直接用 Map 作为 XMLHttpRequest.send() 的发送数据是不合适的。因为,发送之前要做很多封装工作。所以,就出现一个新的概念——FormData。
FormData 可以被当做是一个 Map 来对待,它也有和 Map 类似的函数,例如:get、entries。
有了 FormData,我们可以自定义发送表单数据,可以添加表单没有设置的一项数据。使用 AXIOS 或 AJAX,可以轻松地、自由地把表单数据发送出去。
FormData 提供一个 set(name, value) 函数。它可以添加数据到 FormData 中。而 name 相当于一个表单中属性名为 name 的 input,value 就是用户在 input 中输入的数据。
<input id="uname" type="text" name="uname" />
这个 input 标签用 FormData 表示就是:
let formData = new FormData(); formData.set('hobbies', ['football', 'animation', 'cycling']); formData.get('hobbies');
append(name, value) 也可以添加数据到 FormData 中。
formData.append('hobbies', ['football', 'animation', 'cycling']); formData.get('hobbies');
append 和 set 都可以添加数据到 FormData 中。但是实际使用中有着很大的区别,下面将通过实验来证明。
(一)set
formData.set('hobbies', 'football'); console.log(`第一次 value:${formData.getAll('hobbies')}`); formData.set('hobbies', 'animation'); console.log(`第二次 value:${formData.getAll('hobbies')}`);
(二)append
formData.append('hobbies', 'football'); console.log(`第一次 value:${formData.getAll('hobbies')}`); formData.append('hobbies', 'animation'); console.log(`第二次 value:${formData.getAll('hobbies')}`);
通过实验证明,如果第二次添加的 key 与第一次的 key 是相同的,set 会覆盖第一次 value 中的数据;append 会追加到 value 之后。
多文件上传的前端使用 Vue3 框架。首先,引入 Vue3 和 Axios。
<script src="https://unpkg.com/vue@next"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
因为 FormData 就是用来描述一个表单的,所以,在 html 中甚至都不需要 form 标签。
<div id="app"> <input @change="selected($event)" type="file" multiple /> <button @click="submit">提交</button> </div>
下面是完整的 Vue3 代码:
Vue.createApp({ data() { return { files: [] }; }, methods: { selected(e) { this.files = e.target.files; }, submit() { let formData = new FormData(); for (let index in this.files) { formData.append(`files`, this.files[index]); } axios.post('http://localhost:8080/upload/files', formData, { headers: { 'content-type': 'multipart/form-data' } }); } } }).mount('#app');
(1)当用户选择了文件之后,input 会触发 change 事件,Vue3 使用$event
接收事件的数据。(2)当用户点击提交按钮之后,首先构造一个 FormData 对象。然后把所有的文件对象存放到 FormData 中,因为是多文件,可以用 append 添加一个 key 为 files 的;value 为数组类型的键值对数据。(3)通过 axios.post() 提交数据给后端处理,并且指定 content-type 是 multipart/form-data 格式。
@RequestMapping(value = "/upload/files", consumes = "multipart/form-data") public void uploadFiles(@RequestBody MultipartFile[] files) { System.out.println(files[0].getOriginalFilename()); System.out.println(files[1].getOriginalFilename()); }
前端发送过来的数据是一个数组,一个元素对应一个 MultipartFile。files 一定要和前端 FormData.append() 当中的 name 对应。
打印结果:
表单的默认的 content-type 是 application/x-www-form-urlencoded;charset=UTF-8。对于这样的 content-type,接口只能通过 @RequestParam 来接收这些数据。
@RequestMapping(value = "/upload/user/info", method = RequestMethod.POST, consumes = "application/x-www-form-urlencoded;charset=UTF-8") public void uploadUserInfo(@RequestParam(name = "uname") String uname, @RequestParam(name = "pwd") String pwd) { System.out.println("uname: " + uname + ", pwd: " + pwd); }