引入:小明的困惑
小明是个前端新人,日常和后端交互,要么用 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 个问题:
- 体积膨胀:Base64 编码会让文件体积增加约 33%(比如 100KB 的图片转成 Base64 后约 133KB),浪费带宽;
- 解析复杂:后端需要先解码 Base64 字符串,再转成文件,额外增加处理成本,且不适合大文件(容易内存溢出)。
(2)典型场景
- 纯结构化数据交互:如 “获取用户列表”“提交登录信息”“修改用户昵称” 等无文件的接口;
- 小体积文本数据:如配置项、接口返回的状态信息(如
{ "code": 200, "msg": "success", "data": {} })。
2. Blob:“二进制数据的容器,大文件的专属载体”
Blob(Binary Large Object,二进制大对象)是浏览器提供的二进制数据存储对象,它的底层是 “二进制数据流”,专门用来处理非文本的原始数据。
(1)核心原理
-
数据结构:类似 “文件片段”,包含 2 个核心属性:
size:二进制数据的总字节数;type:MIME 类型(如图片是image/png、Excel 是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet),用于标识数据类型;
-
常见来源:
- 文件上传:
input[type="file"]选中的文件是File对象(File是Blob的子类,继承了Blob的所有属性,额外增加了name(文件名)、lastModified(最后修改时间)等文件专属属性); - 接口响应:用
fetch或axios请求文件时,可通过response.blob()把响应体转成 Blob(如 “下载文件” 时先获取 Blob,再生成下载链接); - 前端生成:如
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 的核心差异表
| 维度 | JSON | Blob |
|---|---|---|
| 数据类型 | 文本类型(字符串) | 二进制类型(原始数据流) |
| 支持数据格式 | 仅 6 种基础结构化类型(无二进制) | 所有二进制数据(图片、视频、文件等) |
| 体积效率 | 传输文件时(Base64)体积膨胀 33% | 原始二进制,体积最小 |
| 传输头 | Content-Type: application/json | 需手动设(如 image/png、application/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 AUpload | Element Plus ElUpload |
|---|---|---|
| 基础用法 | action + name 自动封装 FormData,代码简洁 | 同 AntD Vue,action + name 即可快速上手 |
| 自定义请求 | customRequest 覆盖默认逻辑,支持手动控制 FormData | http-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>
核心逻辑说明
-
禁用默认请求:通过空的
custom-request阻止AUpload自动发请求,由表单统一控制提交; -
文件校验:
before-upload控制格式 / 大小,file-list-max限制数量; -
FormData 打包:
- 普通字段直接
append键值对; - 文件取
originFileObj(原始 File 对象),多文件用同一键名(后端用数组接收);
- 普通字段直接
-
统一提交:一个接口搞定 “普通字段 + 多文件”,无需拆分请求。
四、总结:怎么选?
-
纯文本数据交互(如用户信息、列表查询)→ 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,再也不怕后端 “甩需求” 了~