文件上传原理与实操

一、文件上传原理

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 中,然后继续处理下一个文件。这样可以避免因为一个文件处理失败,导致整个批量上传失败。
相关推荐
Cx330❀2 小时前
线程进阶实战:资源划分与线程控制核心指南
java·大数据·linux·运维·服务器·开发语言·搜索引擎
人道领域2 小时前
【黑马点评日记02】:Session+ThreadLocal实现短信登录
java·开发语言·spring·tomcat·intellij-idea
Bat U2 小时前
JavaEE|计算机是如何工作的
java·人工智能
许彰午2 小时前
# 政务表单动态建表?运行时DDL引擎,前端拖完字段后端直接建
java·前端·后端·架构·政务
我登哥MVP2 小时前
【Spring6笔记】 - 13 - 面向切面编程(AOP)
java·开发语言·spring boot·笔记·spring·aop
宸津-代码粉碎机2 小时前
Spring Boot 4.0 进阶实战+源码解析系列(持续更新)—— 从落地到源码,搞定面试与工作
java·人工智能·spring boot·后端·python·面试
沐雪轻挽萤2 小时前
2. C++17新特性-结构化绑定 (Structured Bindings)
java·开发语言·c++
java1234_小锋2 小时前
Java高频面试题:Kafka的消费消息是如何传递的?
java·开发语言·mybatis
滴滴答答哒2 小时前
c#将平铺列表转换为树形结构(支持孤儿节点作为独立根节点)
java·前端·c#