vue2+springboot通过 FormData 手动封装图片数据上传

一、Vue2 前端:使用 FormData 上传照片

Vue2 中通过 FormData 手动封装图片数据上传,灵活度更高(可自定义参数、请求头),适配各类后端接口。以下是完整前端实现:

1. 模板部分(含上传按钮、预览)
<template>
  <div class="formdata-upload">
    <h3>FormData 图片上传示例</h3>
    
    <!-- 上传按钮(隐藏原生 input,自定义样式) -->
    <label class="upload-btn">
      选择图片
      <input
        type="file"
        accept="image/jpeg,image/png,image/webp"  <!-- 限制格式 -->
        @change="handleFileChange"
        multiple  <!-- 允许多图上传 -->
        hidden
      >
    </label>

    <!-- 已选择图片预览 -->
    <div class="preview-list" v-if="previewUrls.length > 0">
      <div class="preview-item" v-for="(url, index) in previewUrls" :key="index">
        <img :src="url" alt="预览图">
        <button class="delete-btn" @click="removeImage(index)">删除</button>
      </div>
    </div>

    <!-- 上传按钮 -->
    <el-button type="primary" @click="submitUpload" :disabled="previewUrls.length === 0">
      提交上传
    </el-button>
  </div>
</template>
2. 脚本部分(FormData 封装 + 上传请求)
<script>
import axios from 'axios'  // 需安装:npm i axios

export default {
  name: 'FormDataImageUpload',
  data() {
    return {
      fileList: [],  // 存储选中的 File 对象(用于上传)
      previewUrls: []  // 存储预览 URL(本地 blob 地址)
    }
  },
  methods: {
    /**
     * 选择图片后触发:生成预览 + 保存 File 对象
     */
    handleFileChange(e) {
      const files = e.target.files  // 获取选中的文件列表
      if (!files.length) return

      // 遍历文件,生成预览 URL 并保存 File 对象
      for (let i = 0; i < files.length; i++) {
        const file = files[i]
        // 生成本地预览 URL(blob 格式)
        const previewUrl = URL.createObjectURL(file)
        this.previewUrls.push(previewUrl)
        this.fileList.push(file)
      }

      // 清空 input 值,避免重复选择同一文件不触发 change 事件
      e.target.value = ''
    },

    /**
     * 移除选中的图片
     */
    removeImage(index) {
      // 释放 blob 预览 URL,避免内存泄漏
      URL.revokeObjectURL(this.previewUrls[index])
      // 删除对应的预览 URL 和 File 对象
      this.previewUrls.splice(index, 1)
      this.fileList.splice(index, 1)
    },

    /**
     * 提交上传:用 FormData 封装文件 + 其他参数
     */
    submitUpload() {
      // 1. 创建 FormData 对象
      const formData = new FormData()

      // 2. 封装图片文件(多图上传:append 多次,key 相同)
      this.fileList.forEach((file, index) => {
        // formData.append('file', file)  // 单图/多图(后端用 List<MultipartFile> 接收)
        formData.append(`files[${index}]`, file)  // 多图上传(后端用 MultipartFile[] 接收,可选)
      })

      // 3. 可选:添加其他参数(如用户 ID、图片类型)
      formData.append('userId', localStorage.getItem('userId'))
      formData.append('imageType', 'avatar')  // 自定义业务参数

      // 4. 发送上传请求(axios)
      axios({
        url: 'http://localhost:8080/api/file/formdata-upload',  // 后端接口地址
        method: 'POST',
        data: formData,
        headers: {
          'Content-Type': 'multipart/form-data',  // 必须指定该类型,FormData 自动适配
          'Authorization': 'Bearer ' + localStorage.getItem('token')  // 可选:后端认证 Token
        },
        timeout: 10000
      })
        .then(response => {
          const res = response.data
          if (res.code === 200) {
            this.$message.success('上传成功!');
            // 上传成功后清空列表
            this.fileList = []
            this.previewUrls.forEach(url => URL.revokeObjectURL(url))
            this.previewUrls = []
          } else {
            this.$message.error('上传失败:' + res.msg);
          }
        })
        .catch(error => {
          console.error('上传错误:', error);
          this.$message.error('上传失败,请检查网络!');
        })
    }
  },
  // 组件销毁时释放所有预览 URL
  beforeDestroy() {
    this.previewUrls.forEach(url => URL.revokeObjectURL(url))
  }
}
</script>
3. 样式部分(可选)
<style scoped>
.formdata-upload {
  padding: 20px;
}
.upload-btn {
  display: inline-block;
  padding: 8px 16px;
  background: #409EFF;
  color: white;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 20px;
}
.preview-list {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}
.preview-item {
  position: relative;
  width: 120px;
  height: 120px;
}
.preview-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 4px;
}
.delete-btn {
  position: absolute;
  top: -8px;
  right: -8px;
  background: #F56C6C;
  color: white;
  border: none;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  line-height: 20px;
  text-align: center;
  cursor: pointer;
  font-size: 12px;
}
</style>

二、Spring Boot 后端:接收 FormData 上传的图片

后端接收 FormData 格式的图片,核心是通过 MultipartFile 接收文件流(单图用 MultipartFile,多图用 List<MultipartFile>MultipartFile[])。以下是完整实现:

1. 依赖配置(同之前,无需额外依赖)
<!-- Spring Boot Web (内置文件上传支持) -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 工具类:简化文件操作 -->
<dependency>
  <groupId>commons-io</groupId>
  <artifactId>commons-io</artifactId>
  <version>2.11.0</version>
</dependency>
2. 上传配置(application.yml)
spring:
  servlet:
    multipart:
      max-file-size: 5MB  # 单个文件最大 5MB
      max-request-size: 20MB  # 单次请求最大 20MB(多图时需足够大)
      enabled: true

# 自定义存储配置
upload:
  local:
    path: D:/upload/formdata-images/  # 本地存储路径(Windows)
    # path: /usr/local/upload/formdata-images/  # Linux/macOS
  access-prefix: /formdata-images/  # 前端访问前缀(如 http://localhost:8080/formdata-images/xxx.jpg)
3. 工具类( UploadUtils,生成唯一文件名)
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.File;
import java.util.UUID;

@Component
public class UploadUtils {
    @Value("${upload.local.path}")
    private String localUploadPath;

    /**
     * 生成唯一文件名(UUID + 原后缀)
     */
    public String generateUniqueFileName(String originalFilename) {
        String suffix = StringUtils.getFilenameExtension(originalFilename);
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return suffix != null ? uuid + "." + suffix : uuid;
    }

    /**
     * 获取完整存储路径(创建目录)
     */
    public String getFullStoragePath(String uniqueFileName) {
        File storageDir = new File(localUploadPath);
        if (!storageDir.exists()) {
            storageDir.mkdirs();
        }
        return localUploadPath + uniqueFileName;
    }
}
4. 控制器:接收 FormData 上传请求

支持 单图上传多图上传,同时接收 FormData 中的自定义参数(如 userIdimageType):

import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class FormDataUploadController {
    @Autowired
    private UploadUtils uploadUtils;

    @Value("${upload.access-prefix}")
    private String accessPrefix;

    /**
     * 接收 FormData 多图上传(支持同时传其他参数)
     * 前端 FormData 中图片的 key 为 "files"(与 @RequestParam 一致)
     */
    @PostMapping("/api/file/formdata-upload")
    public ResponseEntity<Map<String, Object>> formDataUpload(
            @RequestParam("files") List<MultipartFile> fileList,  // 接收多图(key=files)
            @RequestParam("userId") String userId,  // 接收自定义参数 userId
            @RequestParam("imageType") String imageType  // 接收自定义参数 imageType
    ) {
        Map<String, Object> result = new HashMap<>();
        List<String> imageUrls = new ArrayList<>();  // 存储所有上传成功的图片 URL

        try {
            // 1. 校验文件列表是否为空
            if (fileList.isEmpty()) {
                result.put("code", 400);
                result.put("msg", "上传失败:未选择图片");
                return ResponseEntity.badRequest().body(result);
            }

            // 2. 遍历文件,逐个处理
            for (MultipartFile file : fileList) {
                // 校验单个文件是否为空
                if (file.isEmpty()) {
                    result.put("code", 400);
                    result.put("msg", "上传失败:存在空文件");
                    return ResponseEntity.badRequest().body(result);
                }

                // 3. 校验文件格式(仅允许 jpg、png、webp)
                String originalFilename = file.getOriginalFilename();
                String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
                if (!suffix.matches("\\.(jpg|jpeg|png|webp)$")) {
                    result.put("code", 400);
                    result.put("msg", "上传失败:" + originalFilename + " 格式不支持(仅允许 JPG/PNG/WebP)");
                    return ResponseEntity.badRequest().body(result);
                }

                // 4. 生成唯一文件名,避免冲突
                String uniqueFileName = uploadUtils.generateUniqueFileName(originalFilename);

                // 5. 保存文件到本地
                String fullStoragePath = uploadUtils.getFullStoragePath(uniqueFileName);
                File destFile = new File(fullStoragePath);
                FileUtils.copyInputStreamToFile(file.getInputStream(), destFile);

                // 6. 构建图片访问 URL
                String imageUrl = accessPrefix + uniqueFileName;
                imageUrls.add(imageUrl);
            }

            // 7. 返回成功结果(包含所有图片 URL)
            result.put("code", 200);
            result.put("msg", "上传成功,共上传 " + imageUrls.size() + " 张图片");
            result.put("data", Map.of("imageUrls", imageUrls, "userId", userId, "imageType", imageType));
            return ResponseEntity.ok(result);

        } catch (IOException e) {
            e.printStackTrace();
            result.put("code", 500);
            result.put("msg", "上传失败:服务器存储异常");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
        }
    }

    /**
     * 单图上传(可选,key=file)
     */
    @PostMapping("/api/file/formdata-single-upload")
    public ResponseEntity<Map<String, Object>> singleFileUpload(
            @RequestParam("file") MultipartFile file  // 单图接收(key=file)
    ) {
        // 逻辑与多图类似,仅需处理单个文件,返回单个 URL 即可
        Map<String, Object> result = new HashMap<>();
        try {
            if (file.isEmpty()) {
                result.put("code", 400);
                result.put("msg", "上传失败:文件为空");
                return ResponseEntity.badRequest().body(result);
            }

            String uniqueFileName = uploadUtils.generateUniqueFileName(file.getOriginalFilename());
            String fullStoragePath = uploadUtils.getFullStoragePath(uniqueFileName);
            FileUtils.copyInputStreamToFile(file.getInputStream(), new File(fullStoragePath));

            String imageUrl = accessPrefix + uniqueFileName;
            result.put("code", 200);
            result.put("msg", "上传成功");
            result.put("data", Map.of("url", imageUrl));
            return ResponseEntity.ok(result);

        } catch (IOException e) {
            e.printStackTrace();
            result.put("code", 500);
            result.put("msg", "上传失败");
            return ResponseEntity.status(500).body(result);
        }
    }
}
5. 静态资源映射(让前端访问本地图片)
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Value("${upload.local.path}")
    private String localUploadPath;

    @Value("${upload.access-prefix}")
    private String accessPrefix;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 映射规则:访问 /formdata-images/** 时,实际访问本地存储路径
        registry.addResourceHandler(accessPrefix + "**")
                .addResourceLocations("file:" + localUploadPath);
    }
}
6. 跨域配置(前端和后端端口不同时需配置)
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")  // 允许跨域的接口路径
                .allowedOrigins("http://localhost:8081")  // 前端项目地址(生产环境改为实际域名)
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);  // 预检请求缓存时间
    }
}

三、核心对接要点

  1. FormData 封装规则

    • 图片文件:通过 formData.append('files', file) 封装(多图多次 append,key 相同),后端用 List<MultipartFile> files 接收;
    • 自定义参数:直接用 formData.append('参数名', 参数值) 封装,后端用 @RequestParam("参数名") 接收。
  2. 请求头配置

    • 必须指定 Content-Type: multipart/form-data,但 axios 会自动根据 FormData 类型设置,无需手动写(手动写可能导致边界符错误)。
  3. 文件格式/大小校验

    • 前端:通过 accept 属性限制格式,通过 file.size 限制大小;
    • 后端:通过配置 max-file-size 限制大小,通过代码校验文件后缀。
  4. 预览 URL 释放

    • 前端通过 URL.createObjectURL(file) 生成的 blob 地址,需在组件销毁或删除图片时用 URL.revokeObjectURL(url) 释放,避免内存泄漏。

四、测试流程

  1. 前端启动 Vue2 项目(端口 8081),后端启动 Spring Boot 项目(端口 8080);
  2. 前端选择图片(支持多图),点击“提交上传”;
  3. 后端接收文件并存储到本地,返回图片访问 URL;
  4. 前端收到成功响应后,清空列表,可通过返回的 URL 访问图片(如 http://localhost:8080/formdata-images/xxx.jpg)。

该方案支持单图/多图上传、自定义参数传递,前端灵活可控,后端兼容标准 multipart/form-data 格式,可直接用于开发环境,生产环境只需替换存储方式(本地 → 云存储)并加强权限校验即可。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值