笔者在项目中有如下需求:使用多个 el-upload
手动上传文件,最后一次性提交。后台要求提交的文件格式是 binary 即二进制形式,实现过程中出现了文件数据以对象方式提交给后端导致报错。
01 Bug 描述
笔者在使用 Vue + Element UI 进行前端开发时遇到多文件上传的需求,我使用 Element UI 的 el-upload
上传器组件实现这一功能,使用如下图所示的官方用例进行开发
基于上述 手动上传 用例实现多个文件上传时,后端响应结果为 上传文件为空
,即文件没有正确发送给后端,如下所示:
02 手动上传多个文件实现过程
首先回顾手动上传多个文件实现过程,主要分为如下三步:
- HTML 页面中引入
el-upload
组件并设置属性 - JS 中监听文件上传事件并作出响应操作
- 使用 Axios 将文件发送给后端处理
2.1 引入 el-upload 组件
el-upload
组件实现了用户点击上传按钮从本地上传文件,这些文件构成的上传文件列表 file-list
将被上传到组件的必选参数 action
指定的地址中。
除了这两个基础参数,该组件还可以通过设置 limit
和 on-exceed
来限制上传文件的个数和定义超出限制时的行为。 更多其他属性设置可以参考官方文档 Upload 上传
<template>
<el-upload
ref="upload"
action="https://jsonplaceholder.typicode.com/posts/"
:auto-upload="false"
:on-change="handleChange"
multiple
:limit="3"
:on-exceed="handleExceed"
:file-list="fileList"
>
<el-button size="small" type="primary" @click="createTask()">上传</el-button>
<template #tip>
<div class="el-upload__tip">
jpg/png files with a size less than 500kb
</div>
</template>
</el-upload>
</template>
2.2 监听文件上传事件
笔者的项目中,多个文件是作为向后端提交的表单的一部分,所以不直接使用 action
属性指定的上传地址,并将 auto-upload
属性设置为 false
实现手动上传。
除此之外,还需要定义 on-change
属性,该属性是监听文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用。我们使用该属性实现,动态获取文件上传组件的上传文件列表,并将文件作为提交表单的一部分。相关内容的 JS 代码如下:
<script>
import { addTask } from '@/api/task'
export default {
data() {
return {
// 提交的表单
taskForm: {
content: null,
title: null,
attachments: [] // 要向后端传输的多个文件
}
}
},
methods: {
// 监听文件状态
handleChange(file, fileList) {
// 将上传文件列表中的所有文件拷贝到 taskForm.attachments 中
fileList.forEach(file => {
this.taskForm.attachments.push(file)
})
},
// 监听文件数目上限
handleExceed(files, fileList) {
this.$message.warning(
`The limit is 3, you selected ${
files.length
} files this time, add up to ${files.length + fileList.length} totally`
)
},
// 提交表单表单由三个部分构成 content title 和由多个文件构成的 attachments
createTask() {
console.log('this task', this.taskForm)
this.$refs['taskForm'].validate((valid) => {
if (valid) {
addTask(this.taskForm).then((response) => {
this.$refs.upload.clearFiles()
})
}
})
}
}
}
</script>
2.3 Axios 向后端传送文件
最后使用 @api/task
中定义的 axios 后端接口 addTask
传送数据,需要注意的是 2.2 中的 taskForm
是 JSON 格式的数据,如果后端处理是接收 FormData 格式的数据需要进行转换。笔者的后端要求接收的是 FormData 格式数据,所以使用 transformRequest
方法将其转换。
export function addTask(data) {
return request({
method: 'post',
url: '/task',
headers: {
'Content-Type': 'multipart/form-data'
},
// 将 json 格式的 data 转换成 formData
transformRequest: [
function(data) {
const formData = new FormData()
for (var key in data) {
formData.append(String(key), data[key])
}
}
],
data
})
}
03 追溯 Bug
梳理了实现过程之后,我开始在每个环节中筛查 Bug。
首先检查了 HTML 中 el-upload
属性是否定义正确,主要是将 auto-upload
属性设置为 false
实现手动上传,定义 on-change
属性获取上传文件列表。
然后检查 on-change
属性的 handleChange()
方法是否正确获取了 el-upload
组件的上传文件列表 file-list
, handleChange()
方法添加输出语句如下:
handleChange(file, fileList) {
fileList.forEach(file => {
this.taskForm.attachments.push(file)
})
console.log('attachments', this.taskForm.attachments)
},
输出如下,这里我们可以发现第一个 Bug:笔者项目的后端要求接收的文件类型是二进制格式的,而 attachments
中存放的是一个文件对象包含name, size, uid
等额外字段,而我们仅需要其中的二进制数据 raw
,所以仍然报错。
最后,检查 @api/task
中定义的 axios 后端接口 addTask
,虽然修改上述 Bug 之后 attachments
中都是二进制文件,但基于 2.3 节代码实现的 addTask
传送给后端的 attachments
是一个列表,而非二进制数据,所以仍然报错,如下图所示:
04 解决 Bug
发现上述 Bug 后,我们逐一进行解决,首先正确获取 el-upload
组件中 filelist
文件列表中二进制文件数据,仅获取 file.raw
数据即可,修改如下:
handleChange(file, fileList) {
fileList.forEach(file => {
this.taskForm.attachments.push(file.raw)
})
},
接着我们需要修改 @api/task
中定义的 axios 后端接口 addTask
实现方式,如果直接将二进制文件数据构成的 attachments
作为 formData 的一个键值对传送给后端会被视为一个列表使得后端无法正确解析。为此,我们单独处理 attachments
的 formData 转换,将二进制文件数据分别封装如下:
export function addTask(data) {
return request({
method: 'post',
url: '/task',
headers: {
'Content-Type': 'multipart/form-data'
},
transformRequest: [
function(data) {
const formData = new FormData()
for (var key in data) {
// 单独处理二进制文件数据 attachments
if (key === 'attachments') {
continue
}
formData.append(String(key), data[key])
}
// 单独处理二进制文件数据 attachments
for (var file of data['attachments']) {
formData.append('attachments', file)
}
return formData
}
],
data
})
}
修改上述 Bug 之后再次测试结果如下,可以看到文件通过二进制的方式使用同一个 key attachments
传给后端
响应成功如下所示:
参考资料