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 中的自定义参数(如 userId、imageType):
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); // 预检请求缓存时间
}
}
三、核心对接要点
-
FormData 封装规则:
- 图片文件:通过
formData.append('files', file)封装(多图多次 append,key 相同),后端用List<MultipartFile> files接收; - 自定义参数:直接用
formData.append('参数名', 参数值)封装,后端用@RequestParam("参数名")接收。
- 图片文件:通过
-
请求头配置:
- 必须指定
Content-Type: multipart/form-data,但 axios 会自动根据 FormData 类型设置,无需手动写(手动写可能导致边界符错误)。
- 必须指定
-
文件格式/大小校验:
- 前端:通过
accept属性限制格式,通过file.size限制大小; - 后端:通过配置
max-file-size限制大小,通过代码校验文件后缀。
- 前端:通过
-
预览 URL 释放:
- 前端通过
URL.createObjectURL(file)生成的 blob 地址,需在组件销毁或删除图片时用URL.revokeObjectURL(url)释放,避免内存泄漏。
- 前端通过
四、测试流程
- 前端启动 Vue2 项目(端口 8081),后端启动 Spring Boot 项目(端口 8080);
- 前端选择图片(支持多图),点击“提交上传”;
- 后端接收文件并存储到本地,返回图片访问 URL;
- 前端收到成功响应后,清空列表,可通过返回的 URL 访问图片(如
http://localhost:8080/formdata-images/xxx.jpg)。
该方案支持单图/多图上传、自定义参数传递,前端灵活可控,后端兼容标准 multipart/form-data 格式,可直接用于开发环境,生产环境只需替换存储方式(本地 → 云存储)并加强权限校验即可。
929

被折叠的 条评论
为什么被折叠?



