文件上传部分

1 阅读2分钟

写在前面

依据项目需求,实现了上传图片的基础功能。前端发送的data类型是Formdata。不用额外配置headers的Content-Type。

优化诸如:文件格式限制为图片格式、文件分片等没有涉及。

后续搭建完架构以后再进行优化。

后端

安装依赖

  1. npm install --save multer
  2. UserRouter.js中添加:
// 图片上传
const multer = require('multer')
// 图片存储路径
const upload = multer({dest:'public/avataruploads/'})
UserRouter.post('/adminapi/upload', upload.single('file'),UserController.upload);

UserController添加新方法

注意:后端此处直接将图片和前端本地地址拼接后返回(config)。所以前端不需要重复进行图片地址拼接。

const config = "http://localhost:3000"
...
upload: async (req, res) => {
        const { username, introduction, gender } = req.body
        const token = req.headers["authorization"].split(" ")[1]
        const avatar = req.file ? config + `/avataruploads/${req.file.filename}` : ''
        var payload = JWT.verify(token)
        

        var result = await UserService.upload({ id: payload.id, username, introduction, gender: Number(gender), avatar })
        if (avatar) {
            res.send({
                code: '1',
                data: {
                    username, introduction, gender: Number(gender), avatar
                }
            })
        } else {
            res.send({
                code: '1',
                data: {
                    username, introduction, gender: Number(gender)
                }
            })
        }
    }

UserService中添加

upload: async ({ id, username, introduction, gender, avatar }) => {
        if (avatar) {
            const searchSQL = `UPDATE user 
        SET username='${username}',
        introduction='${introduction}',
        gender='${gender}',
        avatar='${avatar}'
        WHERE id='${id}'`;
            return connectionQuery(searchSQL);
        } else {
            const searchSQL = `UPDATE user 
            SET username='${username}',
            introduction='${introduction}',
        gender='${gender}'
        WHERE id='${id}'`;
            return connectionQuery(searchSQL);
        }
    }

前端

表单及类型定义

export interface ChangeInfoReq{
    username:String,
    gender:Number,
    introduction:String,
    avatar:String,
    file:File|null
}

const { username, gender, avatar, introduction } = userStore.user
const userForm = ref<ChangeInfoReq>({
  username,
  gender,
  avatar,
  introduction,
  file: null
})

新增表单提交函数

const handelChange = (file: File) => {
  userForm.value.avatar = URL.createObjectURL(file)
  userForm.value.file = file
}

const UpdateHandle = async (formEl: FormInstance | undefined) => {
  if (!formEl) return
  await formEl.validate(async (valid: boolean) => {
    if (valid) {
      const params = new FormData()
      if (userForm.value.file !== null) {
        params.append("file", userForm.value.file)
      }
      params.append("username", userForm.value.username as string)
      params.append("avatar", userForm.value.avatar as string)
      params.append("gender", String(userForm.value.gender))
      params.append("introduction", userForm.value.introduction as string)

      const res = await doUpdateUserAPI(params)
      if (res.data.code === "1") {
        ElMessage({ type: 'success', message: '修改成功' })
        userStore.changeInfo(res.data.data)
      } else {
        ElMessage({ type: 'error', message: '修改失败' })
      }
    }
  })
}

二次封装头像上传组件

<script setup lang="ts">
import { defineProps,computed,defineEmits } from 'vue';
import { Plus } from '@element-plus/icons-vue'
const props = defineProps({
    avatar:String
})
const emit = defineEmits(["NoahChange"])


const handelChange = (file:any) =>{
    emit("NoahChange",file.raw)
}
</script>
<template>
<el-upload
    class="avatar-uploader"
    action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
    :show-file-list="false"
    :auto-upload="false"
    :on-change="handelChange"
  >
    <img v-if="props.avatar" :src="props.avatar" class="avatar" />
    <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  </el-upload>

</template>
<style scoped lang="scss">
.avatar-uploader .avatar {
  width: 178px;
  height: 178px;
  display: block;
}
::v-deep(.el-upload) {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}

::v-deep(.el-upload:hover) {
  border-color: var(--el-color-primary);
}

::v-deep(.el-icon.avatar-uploader-icon) {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
  height: 178px;
  text-align: center;
}
.avatar{
    width:178px;
    height:178px;
}

</style>

头像显示

<Upload :avatar="userForm.avatar" @NoahChange="handelChange" />

踩坑

req.body为{}

原因:请求头里面的content-type格式Content-Type: multipart/form-data

前端请求头设置了服务端无法进行解析的MIME类型,导致服务端无法解析请求体,从而req.body显示为空

解决方法:在app.js中先挂载router后进行路由跳转判断

app.use(UserRouter);
app.use((req, res, next) => {
  // token有效,next放行
  // token过期,返回401
  // 例外情况:处于登录页面,原本就没有token
  if (req.url === "/adminapi/user/login") {
    next()
    return
  }
  const token = req.headers["authorization"].split(" ")[1]
  console.log(req.body)
  ...
}