笔者在项目中有如下需求:使用多个 el-upload 手动上传文件,最后一次性提交。后台要求提交的文件格式是 binary 即二进制形式,实现过程中出现了文件数据以对象方式提交给后端导致报错。

01 Bug 描述

笔者在使用 Vue + Element UI 进行前端开发时遇到多文件上传的需求,我使用 Element UI 的 el-upload 上传器组件实现这一功能,使用如下图所示的官方用例进行开发

elementui 上传 单个文件 element上传多个文件_elementui


基于上述 手动上传 用例实现多个文件上传时,后端响应结果为 上传文件为空,即文件没有正确发送给后端,如下所示:

elementui 上传 单个文件 element上传多个文件_vue.js_02

02 手动上传多个文件实现过程

首先回顾手动上传多个文件实现过程,主要分为如下三步:

  • HTML 页面中引入 el-upload 组件并设置属性
  • JS 中监听文件上传事件并作出响应操作
  • 使用 Axios 将文件发送给后端处理

2.1 引入 el-upload 组件

el-upload 组件实现了用户点击上传按钮从本地上传文件,这些文件构成的上传文件列表 file-list 将被上传到组件的必选参数 action 指定的地址中。

除了这两个基础参数,该组件还可以通过设置 limiton-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-listhandleChange() 方法添加输出语句如下:

handleChange(file, fileList) {
      fileList.forEach(file => {
        this.taskForm.attachments.push(file)
      })
      console.log('attachments', this.taskForm.attachments)
    },

输出如下,这里我们可以发现第一个 Bug:笔者项目的后端要求接收的文件类型是二进制格式的,而 attachments 中存放的是一个文件对象包含name, size, uid 等额外字段,而我们仅需要其中的二进制数据 raw ,所以仍然报错。

elementui 上传 单个文件 element上传多个文件_vue.js_03


最后,检查 @api/task 中定义的 axios 后端接口 addTask ,虽然修改上述 Bug 之后 attachments 中都是二进制文件,但基于 2.3 节代码实现的 addTask 传送给后端的 attachments 是一个列表,而非二进制数据,所以仍然报错,如下图所示:

elementui 上传 单个文件 element上传多个文件_elementui 上传 单个文件_04

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 传给后端

elementui 上传 单个文件 element上传多个文件_vue.js_05


响应成功如下所示:

elementui 上传 单个文件 element上传多个文件_elementui 上传 单个文件_06

参考资料

Element UI 官方文档 Upload 上传