一、文件上传原理
1. HTTP 协议下的文件传输逻辑
**文件上传的本质是将本地文件的二进制数据通过 HTTP 协议从客户端传输到服务端。**但普通的 HTTP 请求默认使用的是 application/x-www-form-urlencoded 格式,这种格式只能传输文本,不能传输图片、视频这类二进制文件。
那么该传输二进制文件呢?
答案是用 multipart/form-data 格式。
使用 multipart/form-data 格式进行文件传输的流程如下图所示。
标文件传输的流程题
这种格式是目前 Web 开发中最主流的文件上传方式,Element UI Upload、阿里云 OSS 直传等工具,默认使用的都是这个格式。
2. 前后端的职责分工
-
前端(以 Vue 为例) :
- 提供文件选择界面
- 校验文件的类型、大小,减少无效请求
- 用 FormData API 把文件打包成符合 multipart/form-data 格式的请求体
- 发送 HTTP 请求到后端,同时可以实时显示上传进度
-
后端(以 SpringBoot 为例):
- 接收前端发来的 Multipart 请求
- 用 MultipartFile 接口解析每个上传的文件
- 校验文件的合法性,比如大小、类型是否符合要求
- 对文件内容进行进一步的操作
3. 核心技术概念

了解了以上概念内容之后,下面进入实操环节。
二、文件上传实操
环境介绍:
- 前端框架:Vue3 Composition API + JavaScript
- 后端框架:SpringBoot
2.1 前端实现
2.1.1 单文件上传
先编写前端文件上传的 UI 界面,可以使用 Element-Plus 中的 Upload 上传器组件,也可以手动实现。下面展示的是 UI 组件中的核心部分:
- **文件选择框:**type="file" 表示这是文件选择框
html
<input type="file" @change="handleFileChange" accept=".jpg,.png,.pdf"/>
- **上传按钮:**当没有选择文件时,按钮是禁用状态
html
<button @click="uploadFile" :disabled="!selectedFile" class="upload-btn">
上传文件
</button>
- **进度条:**当上传进度大于 0 时才显示
html
<div class="progress-bar" v-if="uploadProgress > 0">
<div class="progress" :style="{ width: uploadProgress + '%' }"></div>
<span>{{ uploadProgress }}%</span>
</div>
- **上传结果:**显示成功或失败的信息
html
<div class="result" :class="{ success: isSuccess, error: !isSuccess }" v-if="uploadResult">
{{ uploadResult }}
</div>
下面是文件上传的逻辑实现部分。
- 初始化文件上传相关变量
javascript
/* 存储选中的文件 */
const selectedFile = ref(null);
/* 存储上传进度 */
const uploadProgress = ref(0);
/* 存储上传结果 */
const uploadResult = ref('');
/* 标记上传是否成功 */
const isSuccess = ref(false);
- 文件选择的核心方法
event.target.files 是一个 FileList 对象,存储了用户选择的所有文件,每一个 File 对象都包含 type、size 等文件属性。
javascript
const handleFileChange = (event) => {
selectedFile.value = event.target.files[0]; /* 这里是单文件上传还是多文件上传的核心区别 */
/* 清空之前的进度和结果 */
uploadProgress.value = 0;
uploadResult.value = '';
isSuccess.value = false;
};
- 上传文件的核心方法
javascript
const uploadFile = async () => {
if (!selectedFile.value) {
uploadResult.value = '请先选择文件';
isSuccess.value = false;
return;
}
/* 校验文件类型 */
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(selectedFile.value.type)) {
uploadResult.value = '文件类型不支持,请选择 jpg、png 或 pdf 文件';
isSuccess.value = false;
return;
}
/* 校验文件大小,文件的size单位是B,需要进行单位换算 */
const maxSize = 5 * 1024 * 1024;
if (selectedFile.value.size > maxSize) {
uploadResult.value = '文件大小超过限制,最大支持 5MB';
isSuccess.value = false;
return;
}
/*封装 FormData,将文件打包为符合 multipart/form-data 格式的请求体 */
const formData = new FormData();
/* append 方法的第一个参数是字段名,必须和后端接口的 @RequestParam 注解的参数名一致,比如后端接口是 @RequestParam("file") MultipartFile file,这里的字段名就是 "file" */
formData.append('file', selectedFile.value);
try {
// 4. 发送 HTTP 请求到后端接口
const response = await axios.post(
'url', formData,
{
/* 监听上传进度的回调函数 */
onUploadProgress: (progressEvent) => {
/* progressEvent.loaded:已上传的字节数 | progressEvent.total:文件的总字节数 */
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
},
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
uploadResult.value = '上传成功!';
isSuccess.value = true;
} catch (error) {
uploadResult.value = '上传失败:' + (error.response?.data?.message || error.message);
isSuccess.value = false;
}
};
核心内容解释:
- 用 <input type="file"> 实现文件选择,accept 属性可以初步限制文件类型,但这只是浏览器层面的限制,用户可以通过修改文件后缀绕过,所以必须在 JS 中再做一次严格校验。
- FormData 封装:FormData 是浏览器提供的 API,专门用于处理 multipart/form-data 格式的请求。我们需要把选中的文件用 append 方法添加到 FormData 中,这样后端才能正确解析。注意,append 的第一个参数(字段名)必须和后端接口的 @RequestParam 注解的参数名完全一致,否则后端会接收不到文件。
- 进度条实现:onUploadProgress 是 Axios 提供的回调函数,每次上传进度变化时都会触发。我们通过 progressEvent.loaded 和 progressEvent.total 计算出上传进度的百分比,然后将这个百分比绑定到进度条的宽度样式上,这样进度条就会随着上传进度动态变化了。
2.1.2 多文件上传
多文件上传与单文件上传的前端主要区别在两点,一是如何在文件选择方面选择多个文件,二是如何在 JS 代码中处理多个文件。
- 如何选择多个文件
javascript
<input
type="file"
@change="handleFileChange"
accept=".jpg,.png,.pdf"
multiple
/>
给 <input type="file"> 添加 multiple 属性,用户就可以在文件选择器中同时选择多个文件(按住 Ctrl 或 Shift 键可以多选)。
javascript
const handleFileChange = (event) => {
selectedFiles.value = Array.from(event.target.files);
totalProgress.value = 0;
uploadResults.value = [];
};
选择后,event.target.files 会返回一个包含所有选中文件的 FileList 对象,我们需要用 Array.from() 把它转换成数组,这样才能方便地遍历和处理每个文件。
- 如何处理多个文件
javascript
const validFiles = selectedFiles.value.filter(file => {
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
const maxSize = 5 * 1024 * 1024;
return allowedTypes.includes(file.type) && file.size <= maxSize;
});
if (validFiles.length === 0) {
return;
}
遍历所有选中的文件,对每个文件都进行类型和大小校验。如果某个文件不符合要求,就把它的错误信息添加到 uploadResults 中,然后跳过这个文件,继续处理下一个。这样可以避免因为一个文件不符合要求,导致整个批量上传失败。
javascript
const formData = new FormData();
validFiles.forEach(file => {
formData.append('files', file);
});
后端需要用 @RequestParam("files") MultipartFile[] files 来接收多个文件 ------ 这里的 MultipartFile[] 是数组类型,对应前端 FormData 中所有字段名为 files 的文件。注意,前端 FormData 中 append 的字段名必须和后端的参数名一致,否则后端会接收不到文件。
javascript
onUploadProgress: (progressEvent) => {
totalProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
多文件上传的进度条是总进度,计算方式和单文件上传一样,progressEvent.loaded 是所有文件已上传的总字节数,progressEvent.total 是所有文件的总字节数。这样进度条就会随着所有文件的上传进度动态更新了。
2.2 后端实现
2.2.1 文件上传配置
在写接口前,我们需要在 application.properties 文件配置文件中配置文件上传的参数,比如最大文件大小、请求总大小等。
XML
# 文件上传配置
# 单个文件的最大大小(这里设置为 10MB,根据你的业务需求调整)
spring.servlet.multipart.max-file-size=10MB
# 单次请求的总大小(这里设置为 50MB,比如批量上传 5 个 10MB 的文件,总大小就是 50MB)
spring.servlet.multipart.max-request-size=50MB
# 启用文件上传功能(默认是 true,不用改)
spring.servlet.multipart.enabled=true
# 本地文件存储路径(可以是相对路径或绝对路径)
# 相对路径:相对于项目根目录,比如 uploads/ 表示项目根目录下的 uploads 文件夹
# 绝对路径:比如 D:/uploads/(Windows)或 /home/uploads/(Linux)
file.upload.dir=uploads/
这些配置的作用是:
- **spring.servlet.multipart.max-file-size:**限制单个文件的最大大小,超过这个大小的文件会被拒绝。
- **spring.servlet.multipart.max-request-size:**限制单次请求的总大小,比如批量上传多个文件时,总大小不能超过这个值。
- **file.upload.dir:**本地存储的路径,后端会把上传的文件保存到这个路径下。
2.2.2 单文件上传接口
Controller 核心代码如下:
java
@PostMapping("/single")
public ResponseEntity<?> uploadSingleFile(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) {
throw new BussinessException("上传的文件不能为空");
}
/* 校验文件类型 */
String contentType = file.getContentType();
if (contentType == null || !contentType.matches("image/jpeg|image/png|application/pdf")) {
throw new BussinessException("文件类型不支持,请选择 jpg、png 或 pdf 文件");
}
/* 检验文件大小 */
long maxSize = 5 * 1024 * 1024;
if (file.getSize() > maxSize) {
throw new BussinessException("文件大小超过限制,最大支持 5MB");
}
/* ...对文件的其他操作 */
}
后端需要对上传的文件进行二次校验:
- **file.isEmpty():**检查文件是否为空。
- **file.getContentType():**获取文件的 MIME 类型,然后用正则表达式校验是否是允许的类型。
- **file.getSize():**获取文件的大小(单位是字节),检查是否超过最大限制。
2.2.3 多文件上传接口
Controller 核心代码如下:
java
@PostMapping("/multiple")
public ResponseEntity<?> uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) {
if (files == null || files.length == 0) {
throw new BussinessException("请选择要上传的文件");
}
StringBuilder result = new StringBuilder();
for (MultipartFile file : files) {
/* 检查文件是否为空 */
if (file.isEmpty()) {
result.append("文件为空,跳过<br>");
continue;
}
/* 校验文件类型 */
String contentType = file.getContentType();
if (contentType == null || !contentType.matches("image/jpeg|image/png|application/pdf")) {
result.append(file.getOriginalFilename()).append(":文件类型不支持,跳过<br>");
continue;
}
/* 校验文件大小 */
long maxSize = 5 * 1024 * 1024; // 5MB
if (file.getSize() > maxSize) {
result.append(file.getOriginalFilename()).append(":文件大小超过限制,跳过<br>");
continue;
}
/* ...文件的其他操作 */
}
return ResponseEntity.ok(result.toString());
}
- 使用 @RequestParam("files") MultipartFile[] files 接收多个文件,这里的 MultipartFile[] 是数组类型,对应前端 FormData 中所有字段名为 files 的文件。
- 遍历 files 数组,对每个文件都进行和单文件上传一样的校验和保存操作。如果某个文件处理失败,就把错误信息添加到 result 中,然后继续处理下一个文件。这样可以避免因为一个文件处理失败,导致整个批量上传失败。