如何发送文字与图片API

713 阅读6分钟

环境版本:

  • vue-cli@4.15.7
  • element-ui@2.15.8
  • axios@0.27.0
  • egg@2.15.1
  • chrome: 101.0.4951.54(正式版本) (64 位)
  • 前端用 Vue2 element-ui 演示
  • 后端用 Egg 演示

概要

发送api, 携带文字与文件的 2 种方案:

  1. 图片转 base64
  2. 利用 XMLHttpRequestFormData
  3. 利用 fetchFormData

本篇文章介绍的是XMLHttpRequestFormData方式
核心代码在前端submitForm方法中
核心原理看chrome调试工具Network

目标: 发送1次api请求, 后端接收文字与图片

发送携带信息:

avatar: png | jpg //头像
name: string //名字
sex: number //性别
address: string //地址

Web展示

上传表单: image.png

前端传参: image.png

请求头:
image.png

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4qDxe4D0iD6K8HUg

修改表单回显数据: image.png

PostMan展示

image.png

Egg展示

路由app/router.js

router.put('/students/:id', controller.student.studentUpdate);

控制层app/controller/student.js

async studentUpdate() {
 const { ctx, service } = this;
 
 console.log(`method---`, ctx.request.method)
 console.log(`params----`, ctx.params)
 console.log(`body---`, ctx.request.body)
 console.log(`files---`, ctx.request.files)
 
 ctx.body = {
  msg: 'test update'
 }
} 

后端控制台打印:
image.png

ctx.request.body接收文字
ctx.request.files接收图片

实现代码(重要)

前端代码 Vue2.js

核心代码是 submitForm 方法中的 new FormData()new XMLHttpRequest() 对象

<template>
  <div>
    <div class="header flex-end">
      <el-button type="primary" @click="handleClick">增加学生</el-button>
    </div>
    <el-table :data="tableData" style="width: 90%; margin: 0 auto;">
      <el-table-column prop="id" label="ID" width="180" />
      <el-table-column label="学生照片">
        <template slot-scope="scope">
          <el-image class="img-atr" :src="scope.row.avatar" />
        </template>
      </el-table-column>
      <el-table-column prop="name" label="姓名" />
      <el-table-column prop="sex" label="性别" :formatter="formatterSex" />
      <el-table-column prop="address" label="地址" />
      <el-table-column label="操作">
        <template slot-scope="scope">
          <el-button size="mini" @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
          <el-button size="mini" type="danger" @click="handleDelete(scope.$index, scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!--增加 及 编辑 对话框-->
    <el-dialog :title="title" :visible.sync="dialogFormVisible">

      <!--核心表单部分-->
      <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm">
        <el-form-item label="学生照片" prop="avatar">
          <!-- list-type="text" text/picture/picture-card -->
          <!-- :request-upload="submitForm" -->
          <el-upload
            action="#"
            :auto-upload="false"
            ref="upload"
            accept="image/*"
            list-type="text"
            :show-file-list="false"
            :multiple="false"
            :headers="headers"
            :file-list="fileList"
            :on-change="handleChange"
            :on-success="handleSuccess"
          >
            <div class="flex-start">
              <el-image class="img-atr" :src="ruleForm.avatar" />
              <el-button slot="trigger" size="small" type="primary" :style="{'margin-left':'20px'}">选取文件</el-button>
              <!--<el-button style="margin-left: 10px;" size="small" type="success" @click.stop="submitUpload">上传到服务器</el-button>-->
            </div>
            <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
          </el-upload>
        </el-form-item>
        <el-form-item label="学生姓名" prop="name">
          <el-input v-model="ruleForm.name"></el-input>
        </el-form-item>
        <el-form-item label="学生性别" prop="sex">
          <el-radio v-model="ruleForm.sex" :label="1"></el-radio>
          <el-radio v-model="ruleForm.sex" :label="0"></el-radio>
        </el-form-item>
        <el-form-item label="学生地址" prop="address">
          <el-input v-model="ruleForm.address"></el-input>
        </el-form-item>
      </el-form>

      <!--提交表格按钮-->
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitForm('ruleForm')">确 定</el-button>
      </div>
    </el-dialog>

    <!--删除对话框, 二次确认-->
    <el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
      <span>删除后不可以恢复, 请问要删除吗?</span>
      <span slot="footer" class="dialog-footer">
        <el-button @click="dialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="handleDeleteClose">确 定</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
import Cookies from 'js-cookie'
export default {
  data() {
    return {
      //
      tableData: [],
      // 增加 及 编辑 变量...
      isAddOrEdit      : true, // 增加 或 编辑
      dialogFormVisible: false,
      headers          : {
        token: Cookies.get('token')
      },
      fileList         : [],
      ruleForm         : {
        avatar : 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
        name   : '',
        sex    : '',
        address: '',
      },
      rules            : {
        avatar : [{ required: true, message: '请上传学生头像', trigger: 'blur' },],
        name   : [{ required: true, message: '请输入学生姓名', trigger: 'blur' },],
        sex    : [{ required: true, message: '请选择学生性别', trigger: 'blur' },],
        address: [{ required: true, message: '请输入学生地址', trigger: 'blur' },],
      },
      // 删除对话框,显示隐藏
      dialogVisible: false,
    }
  },
  computed: {
    title() {
      return this.isAddOrEdit ? '增加' : '编辑'
    }
  },
  mounted() {
    this.getStudentList();
  },
  methods: {
    // 本文章核心代码部分: 增加 或 编辑 学生
    submitForm(formName) {
      this.$refs[formName].validate(async (valid) => {
        if (valid) {
          const formData = new FormData()
          formData.append('avatar', this.fileList[0]?.raw || this.ruleForm.avatar)
          formData.append('name', this.ruleForm.name)
          formData.append('sex', this.ruleForm.sex)
          formData.append('address', this.ruleForm.address)

          // 核心代码!!! 用 javascript 原生方式请求
          const xhr = new XMLHttpRequest();

          xhr.onerror = function error(e) {
            console.error('报错')
          };

          xhr.onload = () => {
            if (xhr.status < 200 || xhr.status >= 300) {
              console.error('返回不是预期')
              return;
            }
            // 后端返回的结构体是: { code, msg, data }
            const { code } = JSON.parse(xhr.responseText)
            if (code === 200) {
              this.dialogFormVisible = false; //关闭对话框
              this.ruleForm = {
                avatar : '',
                name   : '',
                sex    : '',
                address: '',
              }; //清除字段信息
              this.resetForm(); //清空表单
              this.$refs['upload']?.clearFiles() //清空已上传的文件列表(该方法不支持在 before-upload 中调用)
              this.fileList.length = 0
              this.$message({
                type   : 'success',
                message: '操作成功',
              })
              this.getStudentList() //刷新列表
            }
          };


          // 增加
          if (this.isAddOrEdit) {
            xhr.open('post', `/api/students`, true);
          }
          // 修改
          else {
            const { id } = this.ruleForm;
            formData.append('id', id)
            xhr.open('put', `/api/students/${ id }`, true);
          }

          // xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); //不会解析
          // xhr.setRequestHeader('Content-Type', 'multipart/form-data'); //nodejs.Error: Multipart: Boundary not found
          xhr.send(formData)
        }
        else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName = 'ruleForm') {
      this.$refs[formName]?.resetFields();
    },
    // 本文章没用到此方法
    submitUpload() {
      this.$refs.upload.submit();
    },
    // 核心代码: 钩子方法: 处理 fileList
    handleChange(file, fileList) {
      console.log('handleChange', file, fileList)
      this.fileList[0] = file;
      this.ruleForm.avatar = URL.createObjectURL(file.raw)
    },
    handleSuccess(file, fileList) {
      console.log('handleSuccess', file, fileList)
    },
    // 格式化性别
    formatterSex(row, column, cellValue, index) {
      // console.log(`---`, row, column, cellValue, index)
      return cellValue === 0 ? '女' : '男'
    },
    // 获取学生列表
    async getStudentList() {
      const stuList = await this.$api.studentList()
      // 后端返回的结构体是: { code, msg, data }
      const { data, msg, code } = stuList;
      if (code === 200) {
        this.tableData = data;
      }
      // console.log(`stuList--`, stuList)
    },
    // 弹出增加学生对话框
    handleClick() {
      this.isAddOrEdit = true;
      this.resetForm()
      this.$refs['upload']?.clearFiles() //清空已上传的文件列表(该方法不支持在 before-upload 中调用)
      this.fileList.length = 0
      this.ruleForm = {
        avatar : 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png',
        name   : '',
        sex    : '',
        address: '',
      }; //清除字段信息
      this.dialogFormVisible = true;
    },
    // 弹出编辑学生对话框
    handleEdit(index, row) {
      console.log('handleEdit----', index, row);
      console.log(`handleEdit----fileList---`, this.fileList)
      this.isAddOrEdit = false;
      this.resetForm()
      this.$refs['upload']?.clearFiles() //清空已上传的文件列表(该方法不支持在 before-upload 中调用)
      this.fileList.length = 0
      this.ruleForm = { ...row }
      this.dialogFormVisible = true;
    },
    // 弹出删除学生对话框
    handleDelete(index, row) {
      console.log(index, row);
      this.ruleForm = { ...row }
      this.dialogVisible = true;
    },
    // 二次确认删除
    async handleDeleteClose() {
      const { code, data } = await this.$api.studentDelete(this.ruleForm.id)
      if (code === 200) {
        this.dialogVisible = false; // 关闭对话框
        this.resetForm(); // 清空表单
        this.$refs['upload']?.clearFiles() //清空已上传的文件列表(该方法不支持在 before-upload 中调用)
        this.fileList.length = 0
        this.getStudentList() // 刷新列表
      }
    },
  }
}
</script>
<style scope>
.header {
  padding: 10px 66px;
}

.img-atr {
  width: 40px;
  height: 40px;
  border-radius: 100%;
  overflow: hidden;
}
</style>

后端代码 Egg.js

配置: config/config.default.js

  ...
  multipart : {
   mode: 'file',
  },
  oss       : {
   client: {
    accessKeyId    : '马赛克马赛克马赛克',
    accessKeySecret: '马赛克马赛克马赛克',
    bucket         : '马赛克马赛克马赛克',
    endpoint       : '马赛克马赛克马赛克',
    timeout        : '60s',
   },
  },
  ...

路由层: app/router.js

 ...
 router.post('/students', controller.student.studentAdd); //添加
 router.put('/students/:id', controller.student.studentUpdate); //修改
 ...
};

控制层: app/controller/student.js
主要看两个方法: async studentAddasync studentUpdate

 ...
 //添加
 async studentAdd() {
  const { ctx, service } = this;
  const { avatar, name, sex, address } = ctx.request.body;
  const [file] = ctx.request.files;
  
  // console.log(`method---`, ctx.request.method)
  // console.log(`params----`, ctx.params)
  // console.log(`body---`, ctx.request.body)
  // console.log(`files---`, ctx.request.files)
  
  
  let result;
  //如果有图片, 发送至阿里云oss. 也可以选择自家静态服务器
  if (file) {
   const avatarName = `egg-oss-demo/img-${ Date.now() }-${ Math.random().toString().slice(2) }-${ path.basename(file.filename) }`;
   try {
    result = await ctx.oss.put(avatarName, file.filepath);
   }
   finally {
    await fs.unlink(file.filepath);
   }
  }
  
  
  const bool = await service.student.studentAdd({
   avatar: result?.url || avatar, //阿里云oss图片地址 或 base64字符串
   name,
   sex,
   address
  });
  
  //反馈给前端结果
  if (bool) {
   ctx.body = {
    code: 200,
    msg : '增加学生成功'
   };
  }
  else {
   ctx.body = {
    code: 400,
    msg : '增加学生失败'
   };
  }
  
  
 }
 
 //修改
 async studentUpdate() {
  const { ctx, service } = this;
  const { id } = ctx.params;
  const { avatar, name, sex, address } = ctx.request.body;
  const [file] = ctx.request.files;
  
  // console.log(`method---`, ctx.request.method)
  // console.log(`params----`, ctx.params)
  // console.log(`body---`, ctx.request.body)
  // console.log(`files---`, ctx.request.files)
  
  let result;
  //如果有图片, 发送至阿里云oss. 也可以选择自家静态服务器
  if (file) {
   const avatarName = `egg-oss-demo/img-${ Date.now() }-${ Math.random().toString().slice(2) }-${ path.basename(file.filename) }`;
   try {
    result = await ctx.oss.put(avatarName, file.filepath);
   }
   finally {
    await fs.unlink(file.filepath);
   }
  }
  
  const bool = await service.student.studentUpdate({
   id,
   avatar: result?.url || avatar, //阿里云oss图片地址 或 base64字符串
   name,
   sex,
   address
  });
  
  if (bool) {
   ctx.body = {
    code: 200,
    msg : '修改学生成功'
   };
  }
  else {
   ctx.body = {
    code: 400,
    msg : '修改学生失败'
   };
  }
  
 }
 
 ...

数据层: app/service/student.js

 ...
 
 //添加 avatar是阿里云oss地址
 async studentAdd({ avatar = '', name, sex, address }) {
  const { app } = this;
  const sql = `insert into students (avatar, name, sex, address)
                 values ('${ avatar }', '${ name }', ${ sex }, '${ address }')`
  const result = await app.mysql.query(sql);
  return result.affectedRows === 1;
 }
 
 //修改 avatar是阿里云oss地址
 async studentUpdate({ id, avatar = '', name, sex = -1, address } = {}) {
  const sql = `update students
                 set avatar='${ avatar }',
                     name='${ name }',
                     sex=${ sex },
                     address='${ address }'
                 where id = ${ id } `
  const result = await this.app.mysql.query(sql);
  return result.affectedRows === 1;
 }
 
 ...

核心原理是 new XMLHttpRequest() new FormData()

用原生的XMLHttpRequest发送string类型File类型

修改学生时发送的 http 请求:
image.png 组件el-upload
必须写:auto-upload="false"属性
action="#"属性随便写一个即可
没有:http-request="httpRequest"属性

// 利用 FormData 对象装载数据
const formData = new FormData();
formData.append(filename, File);
formData.append(key, value);
formData.append(key, value);
// javascript 原生请求
const xhr = new XMLHttpRequest();
// some code...
// 详细法定参见本文章前端代码 Vue.js 实现部分
// ...
// 实现数据提交

el-upload 用到的部分很少

如果你想, 你甚至可以使用原生的 <input type=file> 来实现

核心点就是 XMLHttpRequest

表单数据使用 new FormData() 对象中进行传递

请求方式灵活, 可以使用 RESTful

也可以用 fetch new FormData()

const formData = new FormData();
const fileField = document.querySelector('input[type="file"]');

formData.append("username", "abc123");
formData.append("avatar", fileField.files[0]);

fetch("https://example.com/profile/avatar", {
  method: "PUT",
  body: formData,
})
  .then((response) => response.json())
  .then((result) => {
    console.log("Success:", result);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

文章参考于

1. <input type="file"> - HTML: HyperText Markup Language | MDN

2. FormData() - Web API 接口参考| MDN

3. XMLHttpRequest - Web API 接口参考| MDN

4. JavaScript高级程序设计(第3版)

image.png

image.png

image.png

5. element-ui@2.15.8 <el-upload ... > ... </el-upload> 源码

node_modules/element-ui/packages/upload/src/ajax.js

...
export default function upload(option) {
  if (typeof XMLHttpRequest === 'undefined') {
    return;
  }

  const xhr = new XMLHttpRequest();
  const action = option.action;

  if (xhr.upload) {
    xhr.upload.onprogress = function progress(e) {
      if (e.total > 0) {
        e.percent = e.loaded / e.total * 100;
      }
      option.onProgress(e);
    };
  }

  const formData = new FormData();

  if (option.data) {
  console.log(`option.data--`,option.data)
    Object.keys(option.data).forEach(key => {
      formData.append(key, option.data[key]);
    });
  }

  formData.append(option.filename, option.file, option.file.name);

  xhr.onerror = function error(e) {
    option.onError(e);
  };

  xhr.onload = function onload() {
    if (xhr.status < 200 || xhr.status >= 300) {
      return option.onError(getError(action, option, xhr));
    }

    option.onSuccess(getBody(xhr));
  };

  xhr.open('post', action, true);

  if (option.withCredentials && 'withCredentials' in xhr) {
    xhr.withCredentials = true;
  }

  const headers = option.headers || {};

  for (let item in headers) {
    if (headers.hasOwnProperty(item) && headers[item] !== null) {
      xhr.setRequestHeader(item, headers[item]);
    }
  }
  xhr.send(formData);
  return xhr;
}

踩坑记

文字信息添加到:data="uploadData"中, 使用 el-upload 组件自带方法上传

使用this.$refs['upload'].submit()而后将
文字信息写到:data="uploadData"
文件类型写到:file-list="fileList"
虽然 确认增加确认修改 也能实现, 但写的时候遇到很多坑, 如:

  1. 拿到图片src, 转成文件类型要解决跨域问题
  2. 编辑的时候图片回显需求, 你需要将回显的图片src转换成 element-ui 定义的文件结构, 并放在:file-list="fileList"中, 需要有status: "ready"uid: 1652037695212等, 很是繁琐, 不然点击确认修改(表单有必填校验), 没有反应或报错. ( el-upload源码中有关于statusuid的判断 )
  3. :data="uploadData"Object类型, 写的时候不能修改uploadData内存地址, 即不能使用this.uploadData={ ...this.ruleForm }这样类似的语法, 很是麻烦

此图为el-upload 组件触发:on-change钩子时, 打印的文件结构:
image.png

  1. 源码中, 可以看到 el-upload 的请求方法是写死的 post 请求, 首先是不够灵活, 也不能满足 RESTful 风格的 api

综合以上原因, 最终放弃该思路

关于 chrome 浏览器 http

请求头 Content-Type类型:

PayloadRequest Headers
Request PayloadContent-Type: application/json
Form DataContent-Type: application/x-www-form-urlencoded
Form DataContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryNvXralFFBXBRg16b

后端 egg 中app/controller/student.js编写如下代码进行测试说明:

const { ctx, service } = this;
console.log(`method---`, ctx.request.method) // PUT
console.log(`params----`, ctx.params)        // '2'
console.log(`body---`, ctx.request.body)     // 看文章下面的打印结果
console.log(`files---`, ctx.request.files)   // 看文章下面的打印结果

ctx.body = {
 msg: 'test update'
}
请求头 'Content-Type': 'application/x-www-form-urlencoded'

vuesrc/service/index.js

import request from '@/utils/request.js'
...
//修改学生信息
studentUpdate(id, formData) {
 return request({ // axios 实例
  method : 'put',
  url    : `/students/${ id }`,
  headers: {
   'Content-Type': `application/x-www-form-urlencoded;charset=utf-8` // 设置请求头
  },
  data   : formData
 })
},
...

请求类型:
image.png

可以把数据传递这去:
image.png

后端可以在ctx.request.body拿到数据, 但是这样的结构不是我想要的

egg 控制台输出:

method--- PUT
params---- { id: '2' }
body--- {
  '------WebKitFormBoundary0Z1jbcw85KbnADQC\r\nContent-Disposition: form-data; name': '"avatar"; filename="user.jpg"\r\n' +
    'Content-Type: image/jpeg\r\n' +
    '\r\n' +
    '����\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00��\x00�\x00\t\x06\x07\b\x07\x06\t\b\x07\b\n' +
    '\n' +
    '\t\x0B\r\x16\x0F\r\f\f\r\x1B\x14\x15\x10\x16 \x1D"" \x1D\x1F\x1F$(4,$',
  "1'\x1F\x1F-": '-157:::# ?D?8C49:7\x01\n' +
    '\n' +
    '\n' +
    '\r\f\r\x1A\x0F\x0F\x1A7%\x1F%77777777777777777777777777777777777777777777777777��\x00\x11\b\x00�\x00�\x03\x01"\x00\x02\x11\x01\x03\x11\x01��\x00\x1C\x00\x00\x02\x03\x01
\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x02\x03\x06\x07\x01\x00\b��\x00D\x10\x00\x02\x01\x03\x03\x01\x06\x03\x05\x04\x07\x06\x07\x01\x00\x00\x01\x02\x03\x00\x04
\x11\x05\x12!1\x06\x13"AQq2a�\x14#B��\x15R��\x073S����\x16Ubcr�$CT����5��\x00\x19\x01\x00\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x03\x04\x
01\x00\x05��\x00#\x11\x00\x02\x02\x03\x01\x00\x02\x03\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x11\x03\x12!1\x13A\x04"2aQ\x14��\x00\f\x03\x01\x00\x02\x11\x03\x11\x00?\x00�
�U��7vS���\x1F\x11<p*q"����9湜|Ǽ�\x12���yW��2٢D��e�����s�8P?�}cl�\x0E"RC�\x02��Q��u���\x04*�~#�1�4�D�o��J�FT\x1C\x13�\x1A��]6X;��e�
8,\x01\x01��:��R�ϴM4�˸����ڳx�5\x17ݱV�5��8a�p#\x00\x17��}��W�a�\x1Da�\x1C�.蘎�q�\x1E���XO�\x12�\x009�0��nfS"�l^�\x1A�\x16cȑ�m5{;�9f�F���\x
12��`���`c\x00��\x1F㊕�=�-�I"\\3���A���u�=���W.\x16M�P\x0E9�<�����ִ\x1B׊��\x19`�\x0F\x1F\x00cw�\x18#��ir�0�������0�M��2m�o(�=E5�_�
�\x03��i,�O\x13\x16�~U\x7Fwo{�1��T�\x12 P�\b�\x0B�@�\x17w\x10Y�Qd�|�',
  '3��\x1A\x': { ...内容太多作者给省略了... \r\n' +
    '------WebKitFormBoundary0Z1jbcw85KbnADQC\r\n' +
    'Content-Disposition: form-data; name="name"\r\n' +
    '\r\n' +
    '李世民\r\n' +
    '------WebKitFormBoundary0Z1jbcw85KbnADQC\r\n' +
    'Content-Disposition: form-data; name="sex"\r\n' +
    '\r\n' +
    '0\r\n' +
    '------WebKitFormBoundary0Z1jbcw85KbnADQC\r\n' +
    'Content-Disposition: form-data; name="address"\r\n' +
    '\r\n' +
    '大唐\r\n' +
    '------WebKitFormBoundary0Z1jbcw85KbnADQC\r\n' +
    'Content-Disposition: form-data; name="id"\r\n' +
    '\r\n' +
    '2\r\n' +
    '------WebKitFormBoundary0Z1jbcw85KbnADQC--\r\n'
}
files--- undefined

egg 能在 this.ctx.request.body 中拿到数据

this.ctx.request.files 中拿不到数据, 打印结果也是 undefined

但数据结构不是理想中的, 需要解析, 不够完美

请求头 'Content-Type': 'multipart/form-data'

vuesrc/service/index.js

import request from '@/utils/request.js'
...
//修改学生信息
studentUpdate(id, formData) {
 return request({ // axios 实例
  method : 'put',
  url    : `/students/${ id }`,
  headers: {
   'Content-Type': `multipart/form-data` // 设置请求头
  },
  data   : formData
 })
},
...

后端报 500 错误:
image.png

虽然 Payload 中也能把相关信息带过去:
image.png

egg 控制台报错:nodejs.Error: Multipart: Boundary not found

2022-05-09 02:40:06,957 ERROR 23248 [-/127.0.0.1/-/2ms PUT /students/11] nodejs.Error: Multipart: Boundary not found
... ...
...
请求头 'Content-Type': `multipart/form-data; boundary=----${ Date.now() }`

只写multipart/form-data报错提示找不到, 那就在后面加个时间戳.

vuesrc/service/index.js

import request from '@/utils/request.js'
...
//修改学生信息
studentUpdate(id, formData) {
 return request({ // axios 实例
  method : 'put',
  url    : `/students/${ id }`,
  headers: {
   'Content-Type': `multipart/form-data; boundary=----${ Date.now() }` // 加个时间戳
  },
  data   : formData
 })
},
...

请求头中 Content-Type 加上了时间戳:
image.png

Payload 里没有数据:
image.png

加完后虽然 egg 不报错, 但也拿不到数据:
image.png

写在后面

文章写于: 2022年5月8日, 周日
企业级应用需要做进一步做优化迭代
文章写完时已是: 2022年05月09日, 凌晨03:47, 于 掘金社区.
juejin.cn/post/709544…