前端文件上传进化史:从只会调 JSON 接口的菜鸟,到玩转 FormData 与主流组件库上传

277 阅读9分钟

引入:小明的困惑

小明是个前端新人,日常和后端交互,要么用 JSON 传结构化数据,要么传文件时,就靠 Element 的 el-upload 组件,指定个 action 地址,把 Excel 往上一丢;偶尔传大文件,后端给个 Blob 流的处理方式,他也能依葫芦画瓢。

直到有天,又要做文件上传,后端却没给 action 地址,只甩来一句:“你不会用 FormData 吗?” 小明一下懵了 ——JSON、Blob、FormData,还有现在流行的 Element Plus、Ant Design Vue 4.0 上传组件,到底有啥区别?怎么用才对?

一、核心技术深析:JSON 与 Blob 的 “本质差异”

很多新手像小明一样,只知道 “JSON 传数据、Blob 传文件”,却不清楚背后的逻辑差异,这也是遇到复杂上传时卡壳的核心原因。

1. JSON:“文本协议的优等生,二进制的门外汉”

JSON(JavaScript Object Notation)本质是基于文本的轻量级数据交换格式,它的底层是字符串,所有数据都要转成文本形式传输。

(1)核心原理

  • 存储结构:严格的 “键值对” 树形结构,键必须是字符串,值只能是字符串、数字、布尔、数组、对象、null(6 种基础类型);

  • 传输过程:前端用 JSON.stringify() 把 JS 对象转成 JSON 字符串 → 通过 Content-Type: application/json 头传输 → 后端用对应语言的解析库(如 Java 的 Jackson、Python 的 json 模块)转成后端对象;

  • 二进制短板:无法直接存储二进制数据(如图片、视频的原始二进制流)。如果强行把文件转成文本(如 Base64),会导致 2 个问题:

    1. 体积膨胀:Base64 编码会让文件体积增加约 33%(比如 100KB 的图片转成 Base64 后约 133KB),浪费带宽;
    2. 解析复杂:后端需要先解码 Base64 字符串,再转成文件,额外增加处理成本,且不适合大文件(容易内存溢出)。

(2)典型场景

  • 纯结构化数据交互:如 “获取用户列表”“提交登录信息”“修改用户昵称” 等无文件的接口;
  • 小体积文本数据:如配置项、接口返回的状态信息(如 { "code": 200, "msg": "success", "data": {} })。

2. Blob:“二进制数据的容器,大文件的专属载体”

Blob(Binary Large Object,二进制大对象)是浏览器提供的二进制数据存储对象,它的底层是 “二进制数据流”,专门用来处理非文本的原始数据。

(1)核心原理

  • 数据结构:类似 “文件片段”,包含 2 个核心属性:

    1. size:二进制数据的总字节数;
    2. type:MIME 类型(如图片是 image/png、Excel 是 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet),用于标识数据类型;
  • 常见来源:

    1. 文件上传:input[type="file"] 选中的文件是 File 对象(File 是 Blob 的子类,继承了 Blob 的所有属性,额外增加了 name(文件名)、lastModified(最后修改时间)等文件专属属性);
    2. 接口响应:用 fetch 或 axios 请求文件时,可通过 response.blob() 把响应体转成 Blob(如 “下载文件” 时先获取 Blob,再生成下载链接);
    3. 前端生成:如 canvas.toBlob() 把画布内容转成图片 Blob、new Blob([二进制数据], { type: "MIME类型" }) 手动创建 Blob;
  • 传输限制:单独用 Blob 传文件时,需要手动设置 Content-Type(如 image/png),且无法同时携带普通表单字段(如 “用户 ID”“文件描述”)—— 这也是为什么实际开发中很少单独用 Blob 上传,而是搭配 FormData 的原因。

(2)典型场景

  • 大文件分片上传:把 Blob 用 slice() 方法切成多个小 Blob(如每片 5MB),分多次传给后端,避免一次性传输大文件超时;
  • 前端预览文件:如选中图片后,用 URL.createObjectURL(blob) 生成临时 URL,赋值给 <img> 实现预览;
  • 文件下载:后端返回 Blob 流后,前端通过 a 标签的 download 属性触发下载(如 “导出 Excel” 功能)。

3. JSON 与 Blob 的核心差异表

维度JSONBlob
数据类型文本类型(字符串)二进制类型(原始数据流)
支持数据格式仅 6 种基础结构化类型(无二进制)所有二进制数据(图片、视频、文件等)
体积效率传输文件时(Base64)体积膨胀 33%原始二进制,体积最小
传输头Content-Type: application/json需手动设(如 image/pngapplication/pdf
混合传输能力无法同时传文件 + 普通字段单独使用时无法传普通字段
解析成本前后端解析简单(内置库支持)后端需按 MIME 类型解析,成本较高

二、2025 年主流组件库的上传 “黑科技”

现在 Element Plus 和 Ant Design Vue 4.0 都把文件上传做了深度封装,既保留 FormData 的灵活性,又简化了开发者操作。两者核心都是基于 FormData 实现,但 API 设计和细节优化各有侧重,下面结合具体代码对比说明。

1. Ant Design Vue 4.0:AUpload 基础版 —— 开箱即用的 “傻瓜式” 上传

先看小明可能最先接触的 Ant Design Vue 4.0 基础上传写法,也就是他提供的代码示例。这种方式无需手动处理 FormData,组件会自动封装请求,适合简单上传场景:

<template>
  <a-upload
    v-model:file-list="fileList"  <!-- 双向绑定文件列表,实时控制已选文件 -->
    name="file"                   <!-- 关键:FormData 中文件的键名,需与后端一致 -->
    action="https://www.mocky.io/v2/5cc8019d300000980a055e76"  <!-- 后端上传接口地址 -->
    :headers="headers"            <!-- 自定义请求头(如认证信息,示例中是模拟值) -->
    @change="handleChange"        <!-- 监听文件上传状态变化(上传中/成功/失败) -->
  >
    <a-button>
      <upload-outlined></upload-outlined>
      Click to Upload
    </a-button>
  </a-upload>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import type { UploadChangeParam } from 'ant-design-vue';  // TS 类型提示,提升开发体验

// 监听上传状态变化:处理成功/失败提示
const handleChange = (info: UploadChangeParam) => {
  if (info.file.status !== 'uploading') {
    console.log('文件信息/文件列表:', info.file, info.fileList);
  }
  // 上传成功:提示用户
  if (info.file.status === 'done') {
    message.success(`${info.file.name} file uploaded successfully`);
  } 
  // 上传失败:提示错误
  else if (info.file.status === 'error') {
    message.error(`${info.file.name} file upload failed.`);
  }
};

// 双向绑定的文件列表:可用于回显、清空已选文件
const fileList = ref([]);
// 自定义请求头:示例中是模拟的认证信息,实际无认证可删除
const headers = {
  authorization: 'authorization-text',
};
</script>
  • 核心逻辑:组件内部会自动创建 FormData,将选中的文件以 name="file" 为键名添加进去,再发送 POST 请求到 action 地址,完全无需开发者手动处理 FormData 封装。
  • 适合场景:简单的单文件 / 多文件上传(组件默认支持多文件,选文件时按 Ctrl 可多选),无需额外携带普通表单字段(如 “文件分类”“用户 ID”)的场景。
  • 优点:代码极简,TS 类型支持完善,新手易上手;自带文件列表管理、状态提示(成功 / 失败),无需手动封装。

2. Ant Design Vue 4.0:AUpload 进阶版 —— 自定义请求,掌控 FormData

如果小明遇到 “需要附带普通表单字段”(如上传文件时加 “文件用途”),或 “后端没给固定 action 地址” 的场景,就需要用 customRequest 覆盖组件默认请求,手动控制 FormData:

<template>
  <a-upload
    v-model:file-list="fileList"
    accept=".xlsx,.xls"  <!-- 限制仅上传 Excel 文件 -->
    :customRequest="handleCustomUpload"  <!-- 自定义上传逻辑,替代默认 action -->
  >
    <a-button type="primary">
      <upload-outlined /> 上传 Excel(带额外字段)
    </a-button>
  </a-upload>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import axios from 'axios';
import type { UploadRequestOptions } from 'ant-design-vue';  // 自定义请求的 TS 类型

const fileList = ref([]);

// 自定义上传逻辑:完全掌控 FormData 和请求
const handleCustomUpload = async (options: UploadRequestOptions) => {
  const { file, onSuccess, onError } = options;  // 组件传入的关键参数:文件/成功回调/失败回调
  
  // 1. 手动创建 FormData,添加文件和额外字段
  const formData = new FormData();
  formData.append('file', file);  // 文件:键名需与后端一致(同基础版的 name 属性)
  formData.append('filePurpose', 'report');  // 额外字段:文件用途(示例)
  formData.append('userId', '123');  // 额外字段:用户 ID(示例)

  try {
    // 2. 发送请求(无认证可删除 headers)
    const res = await axios.post('/api/upload/excel', formData, {
      headers: {
        // authorization: localStorage.getItem('token'),  // 实际认证信息,示例中无则删除
      },
      // 可选:监听上传进度
      onUploadProgress: (e) => {
        const progress = Math.round((e.loaded / e.total) * 100);
        console.log(`上传进度:${progress}%`);
      },
    });

    // 3. 调用组件成功回调,更新组件状态(如显示“成功”图标)
    onSuccess(res.data, file);
    message.success(`${file.name} 上传成功`);
  } catch (err) {
    // 4. 调用组件失败回调,更新组件状态(如显示“失败”图标)
    onError(err as Error, file);
    message.error(`${file.name} 上传失败`);
  }
};
</script>
  • 核心逻辑:通过 customRequest 跳过组件默认请求,手动构建 FormData(可自由添加文件和普通字段),用 axios 或 fetch 发送请求,最后通过 onSuccess/onError 同步组件状态。
  • 适合场景:需要附带额外表单字段、自定义请求逻辑(如动态接口地址)、监听上传进度的复杂场景。

3. Element Plus:ElUpload—— 简约与灵活并存

Element Plus 的 ElUpload 设计思路与 Ant Design Vue 4.0 类似,基础版同样靠 action 和 name 自动封装 FormData,进阶版靠 http-request 自定义逻辑,适合习惯 Element 生态的开发者:

基础版(类似 AntD Vue 基础上传)

<template>
  <el-upload
    v-model:file-list="fileList"
    action="/api/upload"  <!-- 后端接口地址 -->
    name="file"           <!-- FormData 中文件键名 -->
    :on-success="handleSuccess"
    :on-error="handleError"
  >
    <el-button type="primary" icon="Upload">点击上传</el-button>
  </el-upload>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import type { UploadFile, UploadSuccessParams } from 'element-plus';

const fileList = ref<UploadFile[]>([]);

const handleSuccess = (response: any, file: UploadFile) => {
  ElMessage.success(`${file.name} 上传成功`);
};

const handleError = (error: Error, file: UploadFile) => {
  ElMessage.error(`${file.name} 上传失败`);
};
</script>

进阶版(自定义 FormData,类似 AntD Vue 进阶上传)


<template>
  <el-upload
    v-model:file-list="fileList"
    :http-request="customUpload"  <!-- 自定义请求,替代 action -->
    accept=".pdf"
  >
    <el-button type="primary" icon="Upload">上传 PDF(带额外参数)</el-button>
  </el-upload>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
import type { UploadRequestOptions } from 'element-plus';

const fileList = ref([]);

const customUpload = async (options: UploadRequestOptions) => {
  const formData = new FormData();
  formData.append('file', options.file);
  formData.append('docType', 'contract');  // 额外字段:文档类型

  try {
    const res = await axios.post('/api/upload/pdf', formData);
    options.onSuccess(res.data);  // 同步组件成功状态
    ElMessage.success('上传成功');
  } catch (err) {
    options.onError(err);  // 同步组件失败状态
    ElMessage.error('上传失败');
  }
};
</script>

4. 两大组件库上传能力对比

维度Ant Design Vue 4.0 AUploadElement Plus ElUpload
基础用法action + name 自动封装 FormData,代码简洁同 AntD Vue,action + name 即可快速上手
自定义请求customRequest 覆盖默认逻辑,支持手动控制 FormDatahttp-request 实现相同功能,API 命名不同
TS 类型支持提供 UploadChangeParam UploadRequestOptions 等完善类型提供 UploadFile UploadRequestOptions 类型,体验一致
特色功能内置 “分片上传”“断点续传” 组件(Upload.Slice),适合大文件需手动结合 Blob 实现分片,原生支持较弱
交互细节支持 “拖拽上传”“文件夹上传”,配置项更细致支持拖拽上传,配置项相对简约
适合场景企业级项目、复杂上传场景(大文件、多字段)中小型项目、简单上传场景,追求轻量化

三、Ant Design Vue 4.0 实战:表单 + 多文件(证件照 + 3 张材料)上传

小明遇到的 “新增用户 + 传 4 张图(1 张证件照 + 3 张补充材料)”,是企业开发中典型的 “表单 + 多文件混合提交” 场景 —— 后端只给一个接口,没有单独的 action,需要把 “姓名、手机号” 等普通字段和 “4 个文件” 用 FormData 打包,一次提交。

下面是完整实现代码,包含 “表单校验、文件数量限制、手动封装 FormData、接口提交” 全流程:

<template>
<!-- 表单容器:绑定数据与校验 -->
<a-form
  ref="formRef"
  :model="formData"
  :rules="formRules"
  label-col="8"
  wrapper-col="14"
  @finish="handleSubmit"
>
  <!-- 普通字段:姓名 -->
  <a-form-item name="username" label="姓名">
    <a-input v-model:value="formData.username" placeholder="请输入" />
  </a-form-item>

  <!-- 普通字段:手机号 -->
  <a-form-item name="phone" label="手机号">
    <a-input v-model:value="formData.phone" placeholder="请输入" />
  </a-form-item>

  <!-- 文件1:证件照(限1张) -->
  <a-form-item name="idCard" label="证件照" :help="idCardHelp">
    <a-upload
      v-model:file-list="idCardList"
      :before-upload="checkIdCard"
      :custom-request="() => {}"  <!-- 禁用默认请求 -->
      accept=".png,.jpg"
      :file-list-max="1"
    >
      <a-button><upload-outlined />选证件照</a-button>
    </a-upload>
  </a-form-item>

  <!-- 文件2:补充材料(限3张) -->
  <a-form-item name="materials" label="补充材料" :help="materialHelp">
    <a-upload
      v-model:file-list="materialList"
      :before-upload="checkMaterial"
      :custom-request="() => {}"  <!-- 禁用默认请求 -->
      accept=".png,.jpg,.pdf"
      :file-list-max="3"
      multiple
    >
      <a-button><upload-outlined />选补充材料</a-button>
    </a-upload>
  </a-form-item>

  <!-- 提交按钮 -->
  <a-form-item wrapper-col="12" offset="8">
    <a-button type="primary" html-type="submit">提交</a-button>
  </a-form-item>
</a-form>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { AForm, AFormItem, AInput, AButton, AUpload, message } from 'ant-design-vue';
import { UploadOutlined } from '@ant-design/icons-vue';
import type { FormInstance, UploadFile } from 'ant-design-vue';
import axios from 'axios';

// 1. 核心数据
const formRef = ref<FormInstance>(null);
// 表单普通数据
const formData = reactive({ username: '', phone: '' });
// 文件列表
const idCardList = ref<UploadFile[]>([]);    // 证件照
const materialList = ref<UploadFile[]>([]);  // 补充材料
// 文件校验提示
const idCardHelp = ref('');
const materialHelp = ref('');

// 2. 普通字段校验规则
const formRules = reactive({
username: [{ required: true, message: '必填', trigger: 'blur' }],
phone: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: '手机号格式错', trigger: 'blur' }],
});

// 3. 文件校验:证件照(2MB内、图片格式)
const checkIdCard = (file: File) => {
const isImg = ['image/png', 'image/jpg'].includes(file.type);
const isLt2MB = file.size / 1024 / 1024 <= 2;
if (!isImg) message.error('仅支持PNG/JPG');
if (!isLt2MB) message.error('不超过2MB');
return isImg && isLt2MB;
};

// 4. 文件校验:补充材料(5MB内、图片/PDF)
const checkMaterial = (file: File) => {
const isAllow = ['image/png', 'image/jpg', 'application/pdf'].includes(file.type);
const isLt5MB = file.size / 1024 / 1024 <= 5;
if (!isAllow) message.error('仅支持PNG/JPG/PDF');
if (!isLt5MB) message.error('不超过5MB');
return isAllow && isLt5MB;
};

// 5. 核心:表单提交(封装FormData)
const handleSubmit = async () => {
try {
  // 步骤1:校验普通字段
  await formRef.value?.validateFields();
  // 步骤2:校验文件数量
  if (idCardList.value.length === 0) return message.error('请选证件照');
  if (materialList.value.length === 0) return message.error('请选补充材料');

  // 步骤3:构建FormData(关键!打包所有数据)
  const formData = new FormData();
  // 加普通字段
  formData.append('username', formData.username);
  formData.append('phone', formData.phone);
  // 加证件照(后端键名:idCard)
  formData.append('idCard', idCardList.value[0].originFileObj!);
  // 加补充材料(后端键名:materials,多文件用同一键名)
  materialList.value.forEach(file => formData.append('materials', file.originFileObj!));

  // 步骤4:发请求(替换为实际接口)
  const res = await axios.post('/api/user/add', formData);
  if (res.data.code === 200) {
    message.success('提交成功');
    // 重置
    idCardList.value = [];
    materialList.value = [];
    formRef.value?.resetFields();
  }
} catch (err) {
  message.error('提交失败');
}
};
</script>

核心逻辑说明

  1. 禁用默认请求:通过空的custom-request阻止AUpload自动发请求,由表单统一控制提交;

  2. 文件校验before-upload控制格式 / 大小,file-list-max限制数量;

  3. FormData 打包

    • 普通字段直接append键值对;
    • 文件取originFileObj(原始 File 对象),多文件用同一键名(后端用数组接收);
  4. 统一提交:一个接口搞定 “普通字段 + 多文件”,无需拆分请求。

四、总结:怎么选?

  • 纯文本数据交互(如用户信息、列表查询)→ JSON,简单直接;

  • 单独处理大文件二进制数据(如分片上传底层)→ Blob(组件库内部已封装,开发者少直接用);

  • 文件 + 普通表单字段混合上传 → FormData,灵活全能;

  • 实际开发选组件库:

    • 若用 Ant Design Vue 4.0:简单上传用 action + name 基础版,复杂场景用 customRequest 进阶版;

    • 若用 Element Plus:逻辑一致,基础版快速上手,复杂场景用 http-request 自定义;

    • 若需处理大文件(如 1GB+):优先选 Ant Design Vue 4.0 的 Upload.Slice 组件,减少手动开发成本。

小明后来终于明白:原来他之前用的 el-upload 和 AntD Vue 4.0 的 a-upload,底层都是靠 FormData 实现文件上传;后端不给 action 地址,其实是希望他手动控制 FormData,灵活添加额外字段。现在再遇到上传需求,他既能用组件库快速搭基础功能,也能在需要时手动操作 FormData,再也不怕后端 “甩需求” 了~