你是否注意到,很多前端工程师对 File 和 Blob API 的认知停留在"上传文件"和"下载文件"这个表面?但如果我告诉你,掌握这两个 API 的细节差异,能让你用纯前端完成:大文件断点续传、客户端图片处理、完全离线的数据导出、甚至 P2P 文件传输——听起来是不是有点夸张?
事实上,字节跳动、阿里巴巴这样的大厂在构建 Web 应用时,都深度依赖这两个看似简单的 API。而很多中小团队却把它当成"学过就行"的基础知识。
今天我们不聊"如何上传文件",而是深度剖析 File 和 Blob 的本质差异、内存管理的陷阱、以及在生产环境中真正的应用设计。
第一部分:你真的理解 File 和 Blob 吗?
Blob 是什么?换个角度理解
如果用现实中的比喻,**Blob(Binary Large Object)就像一个"已经打好的包裹"**——它包含了原始的二进制数据,但对这份数据的来源、用途、名字都一无所知。
?? Blob = 原始二进制数据 + MIME 类型
- 不知道文件名
- 不知道修改时间
- 不知道来自哪个文件
而 **File 则像是"贴了标签的包裹"**:
?? File = Blob + 元数据
- 包含 name(文件名)
- 包含 lastModified(修改时间戳)
- 通常来自用户交互(选文件、拖拽等)
关键认知:File 继承自 Blob,所以每个 File 都是 Blob,但不是每个 Blob 都是 File。
// File 是 Blob 的子类
const file = new File(['hello'], 'test.txt', { lastModified: Date.now() });
console.log(file instanceof Blob); // ?? true
console.log(file instanceof File); // ?? true
// 但反过来不行
const blob = new Blob(['hello'], { type: 'text/plain' });
console.log(blob instanceof File); // ?? false
内存视角:它们到底住在哪里?
这是很多人遗漏的关键点——当你从 <input type="file"> 或拖拽获得 File 对象时,浏览器不是真的把文件内容加载到内存里。
浏览器的处理流程:
┌─────────────────────────┐
│ 用户选择文件 │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ 浏览器创建 File 对象 │ ??─ 关键!只是元数据 + 引用
│ ├─ name │
│ ├─ size │
│ ├─ type │
│ └─ lastModified │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ 真实文件数据仍在磁盘上 │ ??─ 尚未加载到内存
│ (或被浏览器缓冲) │
└─────────────────────────┘
只有当你显式调用读取方法(如 FileReader 或 stream())时,才会真正读取数据。这个设计的核心目的是安全:防止恶意 JavaScript 任意访问用户的文件系统。
第二部分:FileReader — 读取的艺术
基础用法(你可能只知道这些)
当用户选择文件时,最常见的操作:
<input type="file" id="fileInput" />
const input = document.getElementById("fileInput");
input.addEventListener("change", () => {
const file = input.files[0];
console.log(`?? 文件名: ${file.name}`);
console.log(`?? 大小: ${(file.size / 1024).toFixed(2)} KB`);
console.log(`?? 类型: ${file.type}`);
});
读取方式的本质区别
FileReader 提供多种读取方式,但底层逻辑完全不同:
读取方式 | 返回值 | 使用场景 | 性能表现 |
|---|---|---|---|
readAsText() | String | 纯文本、JSON、CSV | 需要字符编码转换 |
readAsDataURL() | Data URL | 图片预览、Base64 传输 | ???? 会膨胀 33% |
readAsArrayBuffer() | ArrayBuffer | 二进制处理、加密、图像处理 | 高效,直接操作内存 |
readAsArrayBuffer() + TextDecoder | String | 纯文本(推荐方案) | 更快,避免 FileReader 开销 |
陷阱 1:Data URL 的隐性成本
很多人用 readAsDataURL() 做图片预览,以为这样"轻量级":
// ?? 常见的"快速方案"
reader.onload = () => {
const img = document.createElement("img");
img.src = reader.result; // 这是一个超长的 Data URL
document.body.appendChild(img);
};
reader.readAsDataURL(file);
问题在于:Data URL 会增加约 33% 的数据体积(因为 Base64 编码)。一个 3MB 的图片经过 readAsDataURL(),字符串会膨胀到 4MB。
更好的做法:
// ?? 推荐方案:使用 Object URL
const url = URL.createObjectURL(file);
const img = document.createElement("img");
img.src = url;
document.body.appendChild(img);
// 重要!不用时释放,否则内存泄漏
img.onload = () => URL.revokeObjectURL(url);
性能对比(以 3MB 图片为例):
readAsDataURL(): 产生 4MB 字符串,绑定到 DOM,内存持续占用
Object URL: 浏览器内部优化,内存占用最小,不需要字符串化
陷阱 2:FileReader 的异步陷阱
const reader = new FileReader();
reader.onload = () => {
console.log("?? 读取完成");
};
reader.readAsText(file);
console.log("?? 读取中..."); // 这行会先执行!
FileReader 是完全异步的,没有 Promise 支持(在较新的浏览器中可以使用 File.text() 等现代 API)。如果你需要处理多个文件,很容易陷入回调地狱。
现代方案:
// ?? 使用 File API 的现代方法(已被大多数浏览器支持)
asyncfunction readFileAsText(file) {
returnawait file.text();
}
asyncfunction readFileAsBuffer(file) {
returnawait file.arrayBuffer();
}
// 使用示例
const file = input.files[0];
const content = await readFileAsText(file);
console.log(content);
第三部分:Blob 的真正超能力
场景 1:客户端生成文件并下载
这是字节跳动、阿里云那样的平台常见的需求——用户点击"导出数据",前端直接生成 CSV、JSON、甚至 PDF,不需要任何后端参与。
// 导出 CSV 的完整示例
function exportToCSV(data) {
// 1???? 构造 CSV 字符串
const csvContent = data
.map(row =>Object.values(row).join(','))
.join('\n');
// 2???? 创建 Blob
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
// 3???? 生成临时 URL
const url = URL.createObjectURL(blob);
// 4???? 触发下载
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `data_${Date.now()}.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
// 5???? 清理资源
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// 使用
const mockData = [
{ name: '小明', age: 28, city: '北京' },
{ name: '小红', age: 26, city: '上海' },
{ name: '小刚', age: 30, city: '深圳' }
];
exportToCSV(mockData);
为什么这很强大:
?? 不依赖服务器,减少网络往返(在字节的数据分析平台中广泛使用)
?? 对隐私敏感的数据,处理不离开浏览器
?? 用户选择"导出"到"下载完成"的延迟最小化
场景 2:构造 File 对象,与后端 API 兼容
很多时候,你的后端 API 期望接收 FormData 里的 File 对象。但你的数据来自:
网络请求返回的 Blob
动态生成的内容
Canvas 绘制的图片
解决方案:从 Blob 构造 File
// 场景:从网络获取图片,要上传到另一个服务
asyncfunction transferImage(imageUrl) {
// 1???? 获取图片作为 Blob
const response = await fetch(imageUrl);
const imageBlob = await response.blob();
// 2???? 从 Blob 创建 File
const imageFile = new File(
[imageBlob],
'transferred_image.jpg',
{ type: imageBlob.type, lastModified: Date.now() }
);
// 3???? 通过 FormData 上传(与标准上传无区别)
const formData = new FormData();
formData.append('file', imageFile);
const uploadRes = await fetch('/api/upload', {
method: 'POST',
body: formData
});
returnawait uploadRes.json();
}
关键点:服务器无法区分这个 File 是用户选择的,还是前端动态创建的。这就是 File API 的灵活性——它打破了"文件必须来自用户"的认知。
场景 3:二进制数据的网络传输
当你从网络获取二进制文件(如 PDF、音频、视频)时,如果直接让浏览器处理,可能会触发下载或渲染。但有时你需要检查、处理或转发这个数据:
// 获取 PDF,而不让浏览器默认下载
asyncfunction fetchAndPreviewPDF(pdfUrl) {
const response = await fetch(pdfUrl);
const pdfBlob = await response.blob();
// 1???? 创建临时 URL(不是 Data URL)
const url = URL.createObjectURL(pdfBlob);
// 2???? 在 iframe 或特殊查看器中预览
const iframe = document.createElement('iframe');
iframe.src = url;
document.body.appendChild(iframe);
// 3???? 用户关闭后释放
// URL.revokeObjectURL(url);
}
// 或者,将其转发到另一个服务
asyncfunction forwardBlobToAnotherService(sourceUrl) {
const response = await fetch(sourceUrl);
const blob = await response.blob();
const formData = new FormData();
formData.append('file', blob);
return fetch('/api/process', {
method: 'POST',
body: formData
});
}
为什么比 Data URL 好:
Object URL 不膨胀数据(不需要 Base64 编码)
浏览器内部优化,内存占用小
支持流式处理大文件
第四部分:大文件断点续传的底层逻辑
现在来到 Blob 和 File API 最实用的场景——如何高效地上传几百 MB 或几 GB 的文件。
核心思路:分片 + 并行 + 重传
大文件上传流程(完整版)
┌──────────────────────────────┐
│ 选择 1GB 文件 │
└───────────────┬──────────────┘
│
▼
┌───────────────────────┐
│ 分割成 1MB 的 Chunks │ ??─ 使用 Blob.slice()
│ Chunk 1 / Chunk 2 ... │
└───────────────┬───────┘
│
┌───────────┼───────────┐
│ │ │
▼ ▼ ▼
上传Chunk1 上传Chunk2 上传Chunk3 ??─ 并行上传(3个同时)
│ │ │
└───────────┼───────────┘
│
▼
┌─────────────────┐
│ 服务器校验MD5 │
│ 或验证分片完整性 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 服务器合并分片 │
│ 生成完整文件 │
└─────────────────┘
实现细节
class ResumableUploader {
constructor(file, options = {}) {
this.file = file;
this.chunkSize = options.chunkSize || 1024 * 1024; // 默认 1MB
this.concurrency = options.concurrency || 3; // 并行数
this.uploadedChunks = newSet();
this.uploadUrl = options.uploadUrl;
}
// 分割文件
*chunkGenerator() {
let start = 0;
while (start < this.file.size) {
const end = Math.min(start + this.chunkSize, this.file.size);
yield {
index: Math.floor(start / this.chunkSize),
blob: this.file.slice(start, end),
start,
end
};
start = end;
}
}
// 上传单个分片
async uploadChunk(chunk) {
const formData = new FormData();
formData.append('chunkIndex', chunk.index);
formData.append('chunkBlob', chunk.blob);
formData.append('fileId', this.file.lastModified); // 简单的文件标识
try {
const response = await fetch(this.uploadUrl, {
method: 'POST',
body: formData
});
if (response.ok) {
this.uploadedChunks.add(chunk.index);
returntrue;
}
} catch (error) {
console.error(`分片 ${chunk.index} 上传失败:`, error);
returnfalse;
}
}
// 并行上传所有分片
async uploadAll(onProgress) {
const chunks = Array.from(this.chunkGenerator());
let completed = 0;
for (let i = 0; i < chunks.length; i += this.concurrency) {
const batch = chunks.slice(i, i + this.concurrency);
awaitPromise.all(
batch.map(chunk =>this.uploadChunk(chunk))
);
completed += batch.length;
onProgress?.(completed / chunks.length);
}
returnthis.uploadedChunks.size === chunks.length;
}
}
// 使用示例
const input = document.getElementById('fileInput');
input.addEventListener('change', async (e) => {
const file = e.target.files[0];
const uploader = new ResumableUploader(file, {
uploadUrl: '/api/upload-chunk',
chunkSize: 1024 * 1024, // 1MB
concurrency: 3 // 同时上传 3 个分片
});
uploader.uploadAll((progress) => {
console.log(`?? 上传进度: ${(progress * 100).toFixed(2)}%`);
});
});
关键点:
?? file.slice(start, end) 返回一个新的 Blob,不复制底层数据,只是引用
?? 即使文件是 5GB,分片操作也非常快(O(1))
?? 在网络中断时,只需重传失败的分片,不需要重新上传整个文件
实际应用:字节跳动的云存储、阿里云的 OSS 上传工具,都基于这个原理。
第五部分:流式处理 — 突破内存限制
对于超大文件(如 1GB+ 视频),即使分片上传,单次读取仍可能撑爆内存。这时需要流式处理:
// 流式读取大文件,避免一次性加载
asyncfunction streamLargeFile(file) {
const stream = file.stream();
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
console.log('?? 流式处理完成');
break;
}
// value 是 Uint8Array,大小可控(通常 64KB)
console.log(`?? 处理了 ${value.byteLength} 字节`);
// 在这里处理每个数据块
// 例如:上传、计算哈希、压缩等
}
} finally {
reader.releaseLock();
}
}
与分片上传的区别:
分片是"我主动分割文件"(应用层)
流是"浏览器帮我分割数据读取"(系统层)
场景:
分片上传:需要用户控制块大小、并行度、错误重试
流式处理:处理无法一次性加载的超大文件
第六部分:安全性 — 浏览器的防线
很多人不知道,File 和 Blob API 的设计本身就内置了多层安全机制:
1. 沙箱隔离
// ?? 你无法做到的事情
const files = await navigator.filesystem.getFile('/etc/passwd'); // 不存在此 API
// ?? 你只能读取用户选择的文件
input.addEventListener('change', (e) => {
const file = e.target.files[0]; // 用户授权
});
JavaScript 无法任意访问用户的文件系统。即使有恶意代码,也只能操作用户明确选择的文件。
2. 同源策略 + Object URL
// Object URL 有作用域限制
const objectUrl = URL.createObjectURL(blob);
// ?? 同一页面内可用
const img = document.createElement('img');
img.src = objectUrl;
// ?? 跨域窗口无法访问
window.open(objectUrl); // 另一个窗口打开这个 URL,会被拒绝
Object URL 自动遵守同源策略,且生命周期受限于创建它的文档。
3. CORS 限制
// 如果尝试读取跨域的文件...
fetch('https://another-domain.com/file.bin')
.then(res => res.blob())
.catch(err => {
// ?? 没有 CORS 头会失败
console.log('跨域失败');
});
即使是 Blob,跨域限制仍然适用。
第七部分:常见陷阱与优化
陷阱 1:忘记释放 Object URL
// ?? 内存泄漏代码
for (let i = 0; i < 1000; i++) {
const url = URL.createObjectURL(new Blob(['data']));
// 没有 revokeObjectURL,内存不断增长!
}
// ?? 正确做法
for (let i = 0; i < 1000; i++) {
const url = URL.createObjectURL(new Blob(['data']));
// 使用...
URL.revokeObjectURL(url); // 及时释放
}
陷阱 2:混淆 FileList 和数组
// ?? FileList 不是数组,不能直接使用数组方法
const files = document.getElementById('input').files;
files.map(file => upload(file)); // ?? FileList 没有 map 方法
// ?? 转换为数组
const filesArray = Array.from(files);
filesArray.map(file => upload(file)); // ?? 正确
陷阱 3:Blob 的 MIME 类型问题
// ?? 常见错误:依赖浏览器猜测
const blob = new Blob(['some data']); // 默认 type 是 'application/octet-stream'
// ?? 显式指定 type
const textBlob = new Blob(['hello'], { type: 'text/plain' });
const jsonBlob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const csvBlob = new Blob([csvContent], { type: 'text/csv;charset=utf-8' });
实战案例:完整的上传系统
最后,让我们整合所有知识,实现一个生产级别的文件上传系统(参考字节跳动、阿里云的实现思路):
class ProductionFileUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB
this.maxConcurrency = options.maxConcurrency || 4;
this.maxRetries = options.maxRetries || 3;
this.uploadUrl = options.uploadUrl;
}
// 计算文件哈希(用于秒传和校验)
async calculateFileHash(file) {
const chunks = [];
const chunkSize = 1024 * 1024; // 1MB
let start = 0;
while (start < file.size) {
chunks.push(file.slice(start, start + chunkSize));
start += chunkSize;
}
// 只取第一、中间、最后的分片计算哈希(快速模式)
const samplesToHash = [
chunks[0],
chunks[Math.floor(chunks.length / 2)],
chunks[chunks.length - 1],
new Blob([file.name, file.size, file.lastModified])
];
const hashInput = awaitPromise.all(
samplesToHash.map(chunk => chunk.arrayBuffer())
);
// 简化版:用原生 crypto API
const concatenated = newUint8Array(
hashInput.reduce((acc, buf) => acc + buf.byteLength, 0)
);
let offset = 0;
for (const buf of hashInput) {
concatenated.set(newUint8Array(buf), offset);
offset += buf.byteLength;
}
const hashBuffer = await crypto.subtle.digest('SHA-256', concatenated);
returnArray.from(newUint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// 上传前检查服务器是否已有该文件(秒传)
async checkExist(fileHash) {
const response = await fetch(`${this.uploadUrl}/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileHash })
});
const data = await response.json();
return data.exists; // 返回 true 则秒传成功
}
// 分片上传(带重试机制)
async uploadChunkWithRetry(chunk, retryCount = 0) {
try {
const formData = new FormData();
formData.append('chunkIndex', chunk.index);
formData.append('chunkBlob', chunk.blob);
formData.append('fileId', chunk.fileId);
formData.append('totalChunks', chunk.totalChunks);
const response = await fetch(this.uploadUrl, {
method: 'POST',
body: formData
});
if (!response.ok) {
thrownewError(`HTTP ${response.status}`);
}
return { success: true, index: chunk.index };
} catch (error) {
if (retryCount < this.maxRetries) {
// 指数退避重试
awaitnewPromise(resolve =>
setTimeout(resolve, Math.pow(2, retryCount) * 1000)
);
returnthis.uploadChunkWithRetry(chunk, retryCount + 1);
}
return { success: false, index: chunk.index, error };
}
}
// 完整上传流程
async upload(file, onProgress) {
// 步骤 1:计算哈希
console.log('?? 计算文件哈希...');
const fileHash = awaitthis.calculateFileHash(file);
// 步骤 2:检查秒传
console.log('?? 检查秒传...');
if (awaitthis.checkExist(fileHash)) {
console.log('?? 服务器已有该文件,秒传成功!');
onProgress?.(1);
return { success: true, type: 'instant' };
}
// 步骤 3:分片上传
console.log('?? 开始分片上传...');
const chunks = this.generateChunks(file, fileHash);
const uploadQueue = [...chunks];
let completed = 0;
let uploading = 0;
const failed = [];
returnnewPromise((resolve) => {
const processNext = async () => {
if (uploadQueue.length === 0 && uploading === 0) {
if (failed.length === 0) {
resolve({ success: true, type: 'chunked', hash: fileHash });
} else {
resolve({ success: false, failed });
}
return;
}
while (uploading < this.maxConcurrency && uploadQueue.length > 0) {
uploading++;
const chunk = uploadQueue.shift();
this.uploadChunkWithRetry(chunk).then(result => {
uploading--;
if (result.success) {
completed++;
} else {
failed.push(result.index);
}
onProgress?.(completed / chunks.length);
processNext();
});
}
};
processNext();
});
}
// 生成分片
generateChunks(file, fileId) {
const chunks = [];
const totalChunks = Math.ceil(file.size / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, file.size);
chunks.push({
index: i,
blob: file.slice(start, end),
fileId,
totalChunks
});
}
return chunks;
}
}
// 使用示例
const uploader = new ProductionFileUploader({
uploadUrl: '/api/upload',
chunkSize: 1024 * 1024, // 1MB
maxConcurrency: 4,
maxRetries: 3
});
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
const result = await uploader.upload(file, (progress) => {
console.log(`?? 上传进度: ${(progress * 100).toFixed(2)}%`);
});
if (result.success) {
console.log('?? 上传成功', result);
} else {
console.error('?? 上传失败', result);
}
});
深度对比:何时用 File API,何时用其他方案
┌─────────────────────────────────────────────────────────┐
│ 场景分析:选择合适的文件处理方案 │
├──────────────┬──────────────────────────────────────────┤
│ 场景 │ 推荐方案 │
├──────────────┼──────────────────────────────────────────┤
│ 小文件上传 │ FormData + File │
│ (<5MB) │ 直接POST,简单高效 │
├──────────────┼──────────────────────────────────────────┤
│ 大文件上传 │ 分片 + 并行 + 断点续传 │
│ (>100MB) │ 使用 Blob.slice() + 并发控制 │
├──────────────┼──────────────────────────────────────────┤
│ 超大文件 │ 流式处理 + 分片 │
│ (>1GB) │ file.stream() + chunk 上传 │
├──────────────┼──────────────────────────────────────────┤
│ 客户端生成 │ Blob + Object URL │
│ 数据导出 │ CSV/JSON/PDF 生成后下载 │
├──────────────┼──────────────────────────────────────────┤
│ 图片预览 │ Object URL(绝不用 Data URL) │
│ (任意大小) │ 性能差 33% 会崩溃 │
├──────────────┼──────────────────────────────────────────┤
│ 二进制处理 │ ArrayBuffer + TypedArray │
│ 加密/压缩 │ Crypto API 或第三方库结合 │
├──────────────┼──────────────────────────────────────────┤
│ 离线存储 │ IndexedDB + Blob │
│ 数据同步 │ 配合 Service Worker │
└──────────────┴──────────────────────────────────────────┘
总结:为什么要深度理解 File 和 Blob
性能优化:Object URL vs Data URL,性能差异 30%+
内存管理:Blob.slice() 不复制数据,处理 GB 级文件不会卡顿
架构设计:客户端处理,减少服务器压力(字节跳动的经验)
安全隐患:Object URL 泄漏会导致跨域访问,需要及时释放
用户体验:断点续传 + 秒传,让大文件上传"感觉很快"
很多前端工程师觉得这些 API"太基础"而忽略,但真正的竞争力在于细节。掌握 File 和 Blob,你就掌握了:
?? 如何优化百 MB 级别的网络传输
?? 如何在内存受限的设备上处理大数据
?? 如何构建生产级别的云存储前端
?? 如何给用户提供闪电般的上传体验
常见问题解答
Q:为什么不用 Fetch 的 upload 模式?
A: Fetch 目前没有原生的 upload 进度回调,只能用 XMLHttpRequest。对于分片上传,你需要自己控制并发和重试逻辑,这恰好是我们上面实现的。
Q:Object URL 和 Data URL 有什么本质区别?
A:
Data URL:整个内容编码为字符串,占用内存大、字符串膨胀 33%、不适合大文件
Object URL:浏览器内部引用,占用内存小、性能高、适合任何大小的 Blob
Q:File 和 Blob 的内存什么时候释放?
A:
Blob:当没有引用时,垃圾回收自动清理
Object URL:必须显式调用 URL.revokeObjectURL() 释放,否则内存泄漏
Q:如何在离线状态下保存大文件?
A: 使用 IndexedDB 配合 Blob 存储:
const db = await openDB('myapp');
const tx = db.transaction('files', 'readwrite');
await tx.objectStore('files').add({ name: 'data', blob: largeBlob });
Q:Blob 可以跨域上传吗?
A: 可以,Blob 本身不受 CORS 限制,但取决于服务器 API 的 CORS 策略。
原文链接:https://mp.weixin.qq.com/s/tTmc1pEdWiGhT43KBfcUzA

