【聚焦前端实战】后端让我把图片文件和字段一起上传了,我懵的掏出了祖传的FormData

197 阅读3分钟

大家好,欢迎来到【聚焦前端实战】系列专栏,我是江子麟

文件上传是一个常见的功能,图片上传则是其中最常用到的,对于前端新手来说这也是从字段的增删改查到图片的增删改查的一个飞跃!一般单个图片的上传很简单,许多组件库都可以直接支持,但是如果要把字段和图片文件一起上传那就不支持了,当然也不能像普通的字段上传一样使用JSON,这个时候就要掏出我们的FormData格式了!

FormData

**FormData**接口提供了一种表示表单数据的键值对 key/value的构造方式,如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。

MDN指引:developer.mozilla.org/zh-CN/docs/…

常用方法

FormData.append()

向 FormData 中添加新的属性值,FormData 对应的属性值存在也不会覆盖原值,而是新增一个值,如果属性不存在则新增一项属性值。

FormData.delete()

从 FormData 对象里面删除一个键值对。

FormData.entries()

返回一个包含所有键值对的iterator对象。

FormData.get()

返回在 FormData 对象中与给定键关联的第一个值。

FormData.getAll()

返回一个包含 FormData 对象中与给定键关联的所有值的数组。

FormData.has()

返回一个布尔值表明 FormData 对象是否包含某些键。

FormData.set()

给 FormData 设置属性值,如果FormData 对应的属性值存在则覆盖原值,否则新增一项属性值。

FormData.values()

返回一个包含所有值的iterator对象。

如何使用

在工具中的FormData

以APIPOST7为例 这个body里的参数大家都不陌生吧,想必用的最多的就是json但是这里就是formdata

image.png

代码中使用FormData

加入字段属性

这个是构造函数,所以我们要new 一个formdata 如何再把数据用键值对的方式一个个用appendset 方法加入数据

const formData = new FormData();
formData.append(name, value);
formData.append(name, value, filename);

formData.set(name, value);
formData.set(name, value, filename);

加入文件(图片)

/**
 * JS对象转FormData(一般供上传使用)
*@paramobject
*@author江子麟
*@return{FormData}
*/
export function getFormData(object) {
    const formData = new FormData()
Object.keys(object).forEach(key => {
        const value = object[key]
        if (Array.isArray(value)) {
            value.forEach((subValue, i) =>
                formData.append(key + `[${i}]`, subValue)
            )
        } else {
            formData.append(key, object[key])
        }
    })
    return formData
}

加入 File 文件

首先知道一点:File继承了 Blob

然后继续说重点:

这里文件的组件以最常用的ElmentPlus 的为例

这边的例子是头像上传,那么有以下几点必要的或为了用户体验需要考虑。

  • 图片只能限制有一张
  • 不能直接上传,需要我们自己上传
  • 如果已经有了一张,继续添加并删除上一张选择的图片
  • 限制图片格式
  • 限制图片大小

根据我的实践经验,上述的每一步基本都是坑。

直接上代码,不懂的prop可以移步组件官网,好吧其实我也会备注一下

组件定义

<el-form-item prop="avatar">

  <el-upload		
      action="#" 
      list-type="picture-card"
      accept="image/png, image/jpeg, image/jpg"
      :auto-upload="false"
      :on-exceed="coverImg"
      :on-change="changeImg"
      v-model:file-list="fileList"
      ref="avatarImg"
      :limit="1"
  >
    <template #default>
      <el-icon style="position: absolute"><Plus /></el-icon>
      <img style="opacity: 50%" class="el-upload-list__item-thumbnail" :src="user.avatarUrl" alt="" />
    </template>

  </el-upload>
</el-form-item>

action="#" 地址为空

auto-upload 自动上传

on-exceed 超出limit 钩子

on-changed 选择的文件改变的钩子

file-list 选择的文件存放的钩子

默认的template 中改变的是文件选择框的样式 大概样式如下

image.png

函数定义

changeImg(file) {
  const isIMAGE = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png' || file.raw.type === 'image/jpg'
  const isLt5M = file.raw.size / 1024 / 1024 < 5
  if (!isIMAGE) {
    this.$message({
      showClose: true,
      message: '请选择 jpg、png 格式的图片',
      type: 'warning'
    })
    const currIdx = this.fileList.indexOf(file);
    this.fileList.splice(currIdx, 1);
    return false
  }
  if (!isLt5M) {
    this.$message({
      showClose: true,
      message: '图片大小不能超过 5MB',
      type: 'warning'
    })
console.log(this.fileList[0])
console.log(this.fileList[1])

    const currIdx = this.fileList.indexOf(file);
    this.fileList.splice(currIdx, 1);
    return false
  }
  if (this.limit){
    this.limit = false;
    this.fileList.shift();
  }
},
/**
 *替换覆盖图片(限制只能使用一张图片时)
*@paramfiles
*/
coverImg(files){
  // this.$refs.avatarImg.clearFiles();
  this.$refs.avatarImg.handleStart(files[0]);
  this.limit = true;
  // console.log(files[0])
},

上面这个有一个坑,我利用limit 变量解决了

BUG在于:这个函数的两个钩子的先后顺序是先判断是否超出限制再进入change的钩子,从逻辑上来讲是没问题的,因为我们需要不超过再去改变(加入),可问题在于我们需要做如果已经有了一张,继续添加并删除上一张选择的图片,这里就会有一个问题,当我先执行coverImg() 删除了先前的一张再加入新的哪张,然后changImg() 中文件格式或文件大小有问题,他还是会删除这张新图片,这就会导致此时再fileList中没有已选择的图片了,这与我们想提高用户体验的初心相反。

最后代码处理的思路是:超出限制时,把新选择的图片加入数组,并且将变量limit 变成true 代表当前已经超出了限制,如果图片不合法则删除该图片,如果合法则删除第一张图片(最初的图片)

最后上传接口的代码

updateInfo() {
  this.$refs.form.validate((valid) => {
    if (valid) {
      showLoading('updateInfo',{text:'正在修改个人信息'})
      const upDateData = {
        phone: userStore.phoneNumber,
      };
      upDateData.singerName = this.upDateUser.singerName;
      upDateData.information = this.upDateUser.information;
      upDateData.introduction = this.upDateUser.introduction;
      //将级联选择器中选出的array变成string给后端
      if (typeof upDateData.information !== "string"){
        upDateData.information = upDateData.information[0]
      }
      const formData = getFormData(upDateData);
      //将文件加入formData
      if (this.fileList.length !== 0) {
        formData.append("avatarImg", this.fileList[0].raw, this.fileList[0].name)
      }
      //发送后端请求
SingRequest.post('/singer/update-info-singer',formData,{
        headers:{'Content-Type':'multipart/form-data'},
        timeout:10000
      }).then(res => {
        for (const resKey in res.data) {
          const value = res.data[resKey];
console.log(value)
          if (value !== null && resKey !== "id" && resKey !== "phone"){
            // user和store中的用户信息响应式 同步更改
            this.user[resKey] = value;
          }
        }
        this.upDateUser =JSON.parse(JSON.stringify(userStore.userData));
        SingMessage("成功了","success");
        //移除上传选择框
        this.fileList.length = 0;
        this.update = true
      }).catch(res => {
        SingMessage("更新失败"+res.msg+"请重试","error");
      }).finally(()=>{
        //关闭加载框
        hideLoading('updateInfo')
      })

    }
  })
},

注意点:

  • 将文件加入到formdata
  • 请求头设置为'Content-Type':'multipart/form-data'

我这边用了pinia中全局存放用户变量,然后展示信息和store中的响应式双向绑定,用于存放修改数据的upDateUser 深拷贝,为了不污染未改变前的真实数据,最后this.fileList.length = 0; 成功后将选择文件清楚,并且跳转出改变的页面(v-if 控制),其实这里的很多东西都是为了用户体验,如果不考虑用户体验的话,改变完直接刷新页面,重新问后端要一下数据库内已经被改变的新用户数据就行了

后面会继续更新上传前剪切图片,压缩图片。