
你是否注意到,很多前端工程师对 File 和 Blob API 的认知停留在"上传文件"和"下载文件"这个表面?但如果我告诉你,掌握这两个 API 的细节差异,能让你用纯前端完成:大文件断点续传、客户端图片处理、完全离线的数据导出、甚至 P2P 文件传输——听起来是不是有点夸张?
事实上,字节跳动、阿里巴巴这样的大厂在构建 Web 应用时,都深度依赖这两个看似简单的 API。而很多中小团队却把它当成"学过就行"的基础知识。
今天我们不聊"如何上传文件",而是深度剖析 File 和 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 任意访问用户的文件系统。
当用户选择文件时,最常见的操作:
<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 开销 |
很多人用 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: 浏览器内部优化,内存占用最小,不需要字符串化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);
这是字节跳动、阿里云那样的平台常见的需求——用户点击"导出数据",前端直接生成 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);
为什么这很强大:
很多时候,你的后端 API 期望接收 FormData 里的 File 对象。但你的数据来自:
解决方案:从 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 的灵活性——它打破了"文件必须来自用户"的认知。
当你从网络获取二进制文件(如 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 好:
现在来到 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,不复制底层数据,只是引用实际应用:字节跳动的云存储、阿里云的 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 的设计本身就内置了多层安全机制:
// ❌ 你无法做到的事情
const files = await navigator.filesystem.getFile('/etc/passwd'); // 不存在此 API
// ✅ 你只能读取用户选择的文件
input.addEventListener('change', (e) => {
const file = e.target.files[0]; // 用户授权
});
JavaScript 无法任意访问用户的文件系统。即使有恶意代码,也只能操作用户明确选择的文件。
// Object URL 有作用域限制
const objectUrl = URL.createObjectURL(blob);
// ✅ 同一页面内可用
const img = document.createElement('img');
img.src = objectUrl;
// ❌ 跨域窗口无法访问
window.open(objectUrl); // 另一个窗口打开这个 URL,会被拒绝
Object URL 自动遵守同源策略,且生命周期受限于创建它的文档。
// 如果尝试读取跨域的文件...
fetch('https://another-domain.com/file.bin')
.then(res => res.blob())
.catch(err => {
// ❌ 没有 CORS 头会失败
console.log('跨域失败');
});
即使是 Blob,跨域限制仍然适用。
// ❌ 内存泄漏代码
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); // 及时释放
}
// ❌ FileList 不是数组,不能直接使用数组方法
const files = document.getElementById('input').files;
files.map(file => upload(file)); // ❌ FileList 没有 map 方法
// ✅ 转换为数组
const filesArray = Array.from(files);
filesArray.map(file => upload(file)); // ✅ 正确
// ❌ 常见错误:依赖浏览器猜测
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);
}
});
┌─────────────────────────────────────────────────────────┐
│ 场景分析:选择合适的文件处理方案 │
├──────────────┬──────────────────────────────────────────┤
│ 场景 │ 推荐方案 │
├──────────────┼──────────────────────────────────────────┤
│ 小文件上传 │ 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 │
└──────────────┴──────────────────────────────────────────┘
很多前端工程师觉得这些 API"太基础"而忽略,但真正的竞争力在于细节。掌握 File 和 Blob,你就掌握了:
✅ 如何优化百 MB 级别的网络传输
✅ 如何在内存受限的设备上处理大数据
✅ 如何构建生产级别的云存储前端
✅ 如何给用户提供闪电般的上传体验
upload 模式?A: Fetch 目前没有原生的 upload 进度回调,只能用 XMLHttpRequest。对于分片上传,你需要自己控制并发和重试逻辑,这恰好是我们上面实现的。
A:
A:
URL.revokeObjectURL() 释放,否则内存泄漏A: 使用 IndexedDB 配合 Blob 存储:
const db = await openDB('myapp');
const tx = db.transaction('files', 'readwrite');
await tx.objectStore('files').add({ name: 'data', blob: largeBlob });
A: 可以,Blob 本身不受 CORS 限制,但取决于服务器 API 的 CORS 策略。
📢 如果这篇文章帮助了你,欢迎关注《前端达人》获取更多深度技术内容!
这不仅仅是 API 的使用教程,更是 Web 平台设计哲学的一次深入对话。掌握这些细节,你的代码性能会提升一个量级,用户体验也会质的飞跃。
点赞 👍 | 分享 📤 | 推荐给你的小伙伴 🎯
让我们一起把前端做得更深、更专业、更有竞争力!