前两天同事踩了个特别离谱的坑,折腾大半天没搞明白。
他做大文件上传功能,用 input 文件选择框拿到本地文件,然后用 slice 方法给文件做分片,准备搞分片上传。结果把切好的文件片段传给后端,后端一直报错:找不到文件名。
他当场懵了:原始文件明明有名字,后缀也齐全,怎么一切片,文件名直接凭空消失了?
其实根本原因特别简单:他压根没分清 JS 里的 Blob 和 File。很多前端老手都在这里栽过跟头,今天用大白话给你讲透,以后再也不踩这个坑。
一、先搞懂:Blob 到底是个啥?
你别记那些专业术语,直接把 Blob 理解成一个纯二进制收纳桶。
它只管装图片、视频、文本、文件这类二进制数据,别的啥都不管,只记两个信息:
- size:数据占多大内存
- type:文件的格式类型,比如图片、文本、JSON
它就只是单纯的一堆二进制数据,没有名字、没有修改时间,光秃秃一个数据包。
随便写个简单例子,就能造出一个 Blob:
js
// 把普通文本塞进二进制桶里
const textData = '前端踩坑:Blob和File别搞混';
const myBlob = new Blob([textData], { type: 'text/plain' });
console.log(myBlob.size); // 占用大小
console.log(myBlob.type); // 文件类型
console.log(myBlob.name); // undefined 压根没有文件名!
平时我们用它最多的场景:接口拿图片二进制、生成图片预览链接、做文件下载、文件临时切片。
二、再搞懂:File 又是个啥?
一句话:File 就是带身份证的 Blob。
它天生继承了 Blob 所有的能力,能用 Blob 的所有方法,但是多了两个关键身份信息:
- name:实打实的文件名,比如风景.jpg、简历.pdf
- lastModified:文件最后一次修改的时间戳
我们平时用 <input type="file"> 选本地文件、拖拽上传拿到的,全都是 File 对象。
示例代码:
js
// 文件选择框监听选文件
document.querySelector('#fileInput').addEventListener('change', function(e) {
// 拿到选中的本地文件,是标准 File 对象
const file = e.target.files[0]; console.log(file.name); // 有文件名
console.log(file.size); // 文件大小
console.log(file.type); // 文件格式
console.log(file.lastModified);// 修改时间戳 });
三、一张大白话分清两者区别
| 特点 | Blob | File |
|---|---|---|
| 有没有文件名 | 没有 | 自带 name 文件名 |
| 有没有修改时间 | 没有 | 自带 lastModified |
| 怎么生成 | 代码 new Blob 创建 | 本地选文件、拖拽文件,也能代码手动 new File |
| 适合干啥 | 临时存二进制、切片、预览、下载 | 正式上传、要展示文件名、后端校验文件 |
记住一句核心:File 能当 Blob 用,但 Blob 绝对不能直接当 File 用,因为缺了文件名这些关键信息。
四、三个高频踩坑点,全是真实工作里常遇到的
坑一:文件切片后,文件名直接没了
绝大多数人都以为:我用 File 对象调用 slice 切片,切出来的还是 File。
大错特错!File 调用 slice 切出来的,永远是普通 Blob,直接把文件名给丢没了。
js
// 监听文件选择
document.querySelector('#file').onchange = function (e) {
// 拿到 File(有名字)
const file = e.target.files[0];
console.log('原文件名字:', file.name); // 正常显示:xxx.png
// 切片!!!重点:slice 返回的是 Blob,不是 File!
const chunk = file.slice(0, 1024 * 1024);
// 坑来了:切片后名字没了
console.log('切片后的名字:', chunk.name); // undefined!!!
};
这就是同事遇到的问题:把无名字的 Blob 分片传给后端,后端匹配不到文件名,自然报错合并失败。
解决办法:把切出来的 Blob,重新包装成带文件名的 File 再上传。
js
const file = e.target.files[0];
const chunk = file.slice(0, 1024 * 1024);
// 把 Blob 重新包装成 File,把名字加回去
const realChunk = new File([chunk], file.name, {
type: file.type,
lastModified: file.lastModified
});
console.log(realChunk.name); // 正常显示!
坑二:直接拿 Blob 上传,后端不认
很多人图省事,直接把 Blob 塞进 FormData 传给后端。
你会发现后端收到的文件,名字变成了 blob 或者 blob.bin,没有后缀。后端如果按后缀校验文件类型(只允许 jpg、png、pdf),直接就给拦截报错。
js
// 假设你从接口拿到了图片 Blob
const blob = await fetch('https://picsum.photos/200').then(res => res.blob());
const formData = new FormData();
formData.append('file', blob); // ❌ 直接传 Blob
// 后端收到:文件名为空 / blob / blob.bin,没有后缀
fetch('/upload', { method: 'POST', body: formData });
解决办法:上传前手动给 Blob 包一层 File,自定义文件名和后缀。
js
const blob = await fetch('https://picsum.photos/200').then(res => res.blob());
// ✅ 手动给 Blob 加上文件名和后缀
const file = new File([blob], 'user-avatar.jpg', { type: 'image/jpeg' });
const formData = new FormData();
formData.append('file', file); // 后端能拿到正确文件名
fetch('/upload', { method: 'POST', body: formData });
坑三:图片预览链接不释放,页面越用越卡
用 Blob / File 生成本地预览链接很方便,但很多人只创建不销毁。
链接占用的内存不会自动释放,页面打开多了,内存越堆越高,变得卡顿甚至卡死。
js
const file = e.target.files[0];
const url = URL.createObjectURL(file);
document.querySelector('#img').src = url;
// ❌ 用完不释放,内存一直占用
解决办法:图片加载完成后,立马释放临时链接。
js
const file = e.target.files[0];
const url = URL.createObjectURL(file);
const img = document.querySelector('#img');
img.src = url;
// ✅ 图片加载完成后,立刻释放内存
img.onload = function () {
URL.revokeObjectURL(url);
console.log('预览链接已释放,不占内存啦');
};
五、什么时候该用 Blob?什么时候该用 File?
用 Blob 就行的场景
- 接口返回图片二进制,生成页面预览
- 前端生成文本、JSON,触发浏览器下载
- 文件分片时临时存放切片数据
- Canvas 导出图片二进制数据
必须用 File 的场景
- 用户本地选择文件、拖拽文件上传
- 需要在页面展示文件名、文件大小
- 后端需要校验文件名、文件后缀
- 分片上传要给每块分片绑定原文件信息
Blob 和 File 互相转换,超简单
1、Blob 转 File:给二进制包个文件名外壳
js
// 已有一个无名字的 Blob
const blob = new Blob(['测试内容'], { type: 'text/plain' });
// 包装成带名字的 File
const file = new File([blob], '测试文档.txt', { type: 'text/plain' });
console.log(file.name); // 测试文档.txt
2、File 转 Blob:直接切片就行
js
const file = e.target.files[0];
// 整文件转成 Blob
const blob = file.slice(0, file.size);
实用完整版:文件分片上传正确写法
给你换了一套简洁好懂的业务可用代码,避开文件名丢失的坑:
js
// 分片上传主方法
async function handleFileUpload(file) {
// 每片大小 500KB
const chunkSize = 500 * 1024;
// 计算总片数
const total = Math.ceil(file.size / chunkSize);
// 循环切分每一片
for (let i = 0; i < total; i++) {
const start = i * chunkSize;
const end = start + chunkSize;
// 切片得到 Blob
const chunkBlob = file.slice(start, end);
// 关键:把 Blob 重新包装成 File,保留原文件名
const chunkFile = new File([chunkBlob], file.name, {
type: file.type,
lastModified: file.lastModified
});
// 组装表单数据
const formData = new FormData();
formData.append('chunkFile', chunkFile);
formData.append('chunkIndex', i);
formData.append('totalChunk', total);
formData.append('fileName', file.name);
// 传给后端
await fetch('/api/uploadChunk', {
method: 'POST',
body: formData
});
}
}
最后总结几句大白话
- Blob 就是纯二进制数据桶,只有大小和类型,没有名字;
- File 是带文件名、修改时间的升级版 Blob,本地选文件拿到的都是它;
- File 切片 slice 出来的一定是 Blob,别傻傻以为还是 File;
- 只要是正式上传、要文件名、后端校验,一律用 File;
- Blob 上传一定要手动包装成 File,给它配上文件名;
- 预览用的 ObjectURL,用完一定要手动释放,避免内存泄漏。
以后再做文件上传、分片上传,再也不会被文件名莫名其妙搞报错了。