我在实战里处理过太多文件上传、图片预览、报表导出的需求,几乎每个项目都绕不开 Blob,可不少前端同学一提到 Blob 就只说得出“它是一个二进制对象”,真到了要处理分片上传、实现文件下载、排查内存泄漏的时候,又完全无从下手。这里我把我对 Blob 的完整理解、实际项目中沉淀下来的用法和经验一次性讲清楚,包含可直接抄的代码和踩坑记录,希望能帮你真正把它变成自己的技能。
Blob 是前端处理文件和数据流的基础能力,涵盖文件上传、下载、预览、分片传输等多个高频场景,适合从初级到进阶的所有前端开发者阅读,读完你至少能独立完成大文件分片、纯前端导出文件、图片本地预览这些常见功能。
1. Blob 到底是什么:先撕开前端文件处理的第一层膜
1.1 一句话解释:它就是浏览器里的一堆原始字节
Blob 的全称是 Binary Large Object,翻译过来就是“二进制大对象”,它在浏览器里代表一段不可变的原始二进制数据。你可以把它想象成一个麻袋,你把文件内容、字符串、二进制数据一股脑扔进去,它帮你完整地兜住,之后你需要用的时候再从这个麻袋里往外拿。数据一旦被装进 Blob,就不能直接在原对象上修改,如果想改,只能新建一个 Blob 实例。
这里的关键词是“不可变”。很多人不理解为什么 Blob 要设计成不可变,其实这是为了性能和安全考虑:多个地方可以同时持有同一个 Blob 的引用而不用担心数据被意外改动,浏览器内部也可以放心地做数据共享和缓存。
Blob 最典型的来源有两个:
input[type="file"]用户选择的文件,会生成File对象,而File是Blob的子类。- 代码里手动通过
new Blob([数据])拼接二进制内容。
从这两个来源出发,我们可以做文件上传、图片预览、文件下载、大文件分片、纯前端生成表格等事情,前端日常碰到的文件操作几乎都在 Blob 的能力范围内。
1.2 Blob、File、ArrayBuffer、DataURL:四个容易搞混的兄弟
前端处理数据时有几个长得像但用途完全不同的概念:Blob、File、ArrayBuffer、DataURL,很多新手容易混淆。我用一个生活类比来拆解:
- Blob:是一个“标准集装箱”,装着二进制货物,有自己的尺寸(
size)和类型标签(type),集装箱本身不关心里面是什么。 - File:是“贴着运单的集装箱”,它比 Blob 多了两个字段:
name(文件名)和lastModified(最后修改时间)。文件上传时后端真正关心的是这两个字段,所以File被设计成Blob的子类,天然拥有 Blob 的所有能力。 - ArrayBuffer:是“摆在仓库地板上的原始货物堆”,它是一段固定长度的二进制缓冲区,只能通过
TypedArray或DataView来读写,比 Blob 更底层、更原始。 - DataURL:是“把集装箱拍照打印在纸上”,也就是把二进制数据通过 Base64 编码转成字符串,可以直接塞进
img的src里,但体积会膨胀大约 33%,且完全占用内存。
四者在实际开发中的定位如下表所示:
| 类型 | 本质 | 典型使用场景 | 体积特点 |
|---|---|---|---|
| Blob | 二进制容器 | 分片上传、对象 URL、文件导出 | 与原文件一致 |
| File | 带元信息的 Blob | 表单上传、展示文件名 | 与原文件一致 |
| ArrayBuffer | 底层二进制缓冲区 | 音视频处理、加密、WebSocket 数据 | 与原文件一致 |
| DataURL | Base64 字符串 | 小图片预览、Canvas 导出 | 膨胀约 33% |
理解了这个层次关系,你看到代码里File能调用 Blob 的slice()方法就不会觉得奇怪了,因为子类天然继承父类的全部能力。
1.3 Blob 的构造函数和内部结构
直接看一段最基础的创建代码:
const blob = new Blob(['<html>hello</html>'], { type: 'text/html', endings: 'transparent' });构造函数接收两个参数:
- 第一个参数:一个数组,数组里的每一项可以是字符串、
ArrayBuffer、TypedArray、DataView,甚至是另一个Blob。这些内容会按顺序拼接成最终的数据。这里有个陷阱:很多人以为传['abc', 'def']结果是'abcdef',没错,但如果你传的是数字,比如[123],浏览器会直接报错,因为数字不在合法类型范围内。 - 第二个参数:可选的配置对象,重点关注
type字段,它表示 Blob 的 MIME 类型,默认是空字符串。endings字段只在文本类型时有用,默认'transparent'表示不做换行符转换,如果设为'native'会把换行符转成当前系统的形式。
创建 Blob 之后,你可以访问两个只读属性:
console.log(blob.size); // 字节长度 console.log(blob.type); // MIME 类型size是文件真实大小,单位是字节。比如一个 2MB 的图片,size大约就是 2097152,这个数值是判断文件大小限制、计算分片数量的必备条件。type决定浏览器怎么识别这个数据,比如下载文件时type: 'application/pdf'才能让系统正确关联到 PDF 阅读器,设置错了文件会打不开。
2. 前端为什么离不开 Blob:从文件上传到图片预览的底层逻辑
2.1 文件上传的核心:FormData 与 Blob 的配合
项目里最常遇到的需求就是上传文件,而从浏览器的角度来看,上传的本质就是“把 Blob 交给后端”。我们平时写这类代码时不太注意,但背后其实走的是这样一条链路:
用户选择文件 → 浏览器生成File对象(Blob 子类) → 附加到FormData→fetch或XMLHttpRequest发送给服务器。
const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload', { method: 'POST', body: formData }); const result = await response.json(); console.log(result); });这里有个很容易犯的错误:很多人觉得FormData.append('file', file)里面传的必须是File,其实不是,传Blob也可以,只不过后端收到的文件名会变成blob。实际的ajax库底层在追加数据时,会把File对象原样放进去,如果检测到是普通的Blob,就会补一个默认文件名。所以如果你要传的是new Blob()生成的数据,最好显式地给它一个文件名,做法是构造File:
const myFile = new File([blob], 'filename.txt', { type: 'text/plain', lastModified: Date.now() }); formData.append('file', myFile);这样后端拿到的就是filename.txt,不会出现文件名为空或叫blob的尴尬情况。
2.2 大文件分片上传:分片就是 Blob 切片
上传几个 GB 的大文件时,直接把整个文件丢进FormData发送,很容易超时、内存暴涨、断线后要重头再来。分片上传的核心思想就是“把大文件切成小块,一块一块传,服务器收齐后再合并”。
而分片操作靠的不是FileReader,不是别的工具,正是 Blob 自带的slice()方法。slice()和数组的slice()类似,接收起始字节、结束字节两个参数,返回一个新的 Blob,共享原始数据的一部分:
const chunk = blob.slice(start, end, contentType);contentType可以不传,默认继承原 Blob 的类型。由于切片是只读操作,不会把数据整个复制到内存里,所以大文件分片内存开销非常小,这也是我强烈建议用 Blob 做分片而不是先读取成字符串再切的原因。
2.3 图片预览、视频播放、文件下载:URL.createObjectURL 的功劳
前端要预览用户本地选的图片,有两种方案:一是用FileReader.readAsDataURL转成 Base64 塞给img.src,二是用URL.createObjectURL(file)生成一个blob:开头的临时 URL 塞给img.src。两者的差别很大:
- Base64 方案:把整个文件编码成字符串,体积增大 33%,大图片会卡到怀疑人生。
- Object URL 方案:只是给浏览器一个指向内部二进制数据的引用,不复制数据,几乎零成本生成 URL。
const img = document.querySelector('#preview'); const objectUrl = URL.createObjectURL(file); img.src = objectUrl;同样地,视频播放、PDF 预览、文件下载都用这同一个 API。比如点击按钮下载一个文件:
const download = (content, filename, mime) => { const blob = new Blob([content], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); };这个模式在报表导出、日志下载、模板导出场景里出镜率极高,前端生成内容然后一键下载,完全不需要后端参与。
2.4 纯前端生成文件:文本、JSON、CSV 的导出场景
在很多前端 SDK 和后台管理项目里,统计数据导出是标配功能。过去不少人会把这个请求发给后端,由后端生成文件给下载链接。其实纯前端就能搞定:用new Blob把字符串拼起来,再用上面的下载函数触发下载。
这里有一个血泪教训:导出 CSV 用 Excel 打开时中文会乱码,因为 CSV 本质是文本文件,Excel 默认用 ANSI 编码去解码,而浏览器生成的 Blob 默认是 UTF-8。解决办法是往内容最前面加上\uFEFF,也就是 BOM 头:
const csv = '\uFEFF' + rows.map(row => row.join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });加上 BOM 之后,Excel 会识别出 UTF-8 编码,中文显示就正常了。这个坑几乎每个做过导出功能的前端都会踩一次,先记下来能少走很多弯路。
3. Blob 实战详解:5 个高频场景的完整代码
3.1 用 FileReader 读取 Blob 内容:文本、DataURL、ArrayBuffer
虽然 Blob 可以直接用异步方法读取(后文会讲),但FileReader依然是兼容性最好、最经典的方式,尤其是老项目里大量存在。它有三种常用的读取方法,对应三种需求:
const reader = new FileReader(); // 读取为文本 reader.readAsText(blob, 'utf-8'); // 读取为 DataURL(Base64 字符串) reader.readAsDataURL(blob); // 读取为 ArrayBuffer reader.readAsArrayBuffer(blob); reader.onload = () => { // reader.result 就是读取结果 console.log(reader.result); }; reader.onerror = () => { console.error('读取失败:', reader.error); };关键细节:readAsDataURL和readAsArrayBuffer对于大文件来说都非常吃内存,因为文件内容会被完整编码进内存。如果只是想获取内容长度、做切片上传,根本不需要读取,直接用blob.slice()即可。只有在确实需要解析内容、修改二进制、预览小图片时才用 FileReader。
3.2 用 slice 实现大文件分片与断点续传
直接看一段我在真实项目中用过的分片上传核心代码,思路清晰且可以直接改造:
// 计算分片数量 const CHUNK_SIZE = 2 * 1024 * 1024; // 每片 2MB const chunkCount = Math.ceil(file.size / CHUNK_SIZE); for (let index = 0; index < chunkCount; index++) { const start = index * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); // 用 FormData 上传当前分片 const formData = new FormData(); formData.append('chunk', chunk); formData.append('index', index); formData.append('total', chunkCount); formData.append('filename', file.name); await fetch('/api/upload-chunk', { method: 'POST', body: formData }); // 上传进度 const percent = Math.round(((index + 1) / chunkCount) * 100); console.log(`上传进度:${percent}%`); }这段代码里两个点很关键:
- 切片大小:选择 2MB 是经验值。太大(比如 10MB)会导致单次请求体过大、失败重试成本高;太小(比如 100KB)会增加请求数量,拖慢整体速度。2MB 到 5MB 是我实测下来网络稳定性与速度比较平衡的范围。
- 循环里用 await 串行上传:如果并行上传所有分片,后端合并时需要额外排序和等待。串行实现简单、进度可控,同时支持断点续传:断网失败后记录已上传的
index,下次从index开始续传即可。
断点续传的完整逻辑还要加上“已上传分片查询接口”,上传前先问后端哪些分片已经存在,跳过这些分片,这才是真正的断点续传姿势。
3.3 Blob 转 FormData 上传的完整姿势
在上一节的代码里已经能看到分片就是通过 FormData 传的,这里再补充一个普通场景下 Blob 转 FormData 的完整姿势,特别是如何给纯 Blob 指定文件名:
const blob = new Blob(['hello world'], { type: 'text/plain' }); // 方式一:直接 append,文件名会是 blob,不推荐 const formData1 = new FormData(); formData1.append('file', blob); // 方式二:先转成 File,再 append,后端能拿到正确文件名 const file = new File([blob], 'hello.txt', { type: blob.type }); const formData2 = new FormData(); formData2.append('file', file);我实战中经常用方式二,因为后端很多框架对文件上传接口有强约束:必须拿到带扩展名的文件名,否则判定文件类型失败。比如后端按扩展名决定走图片处理还是文档处理,一个默认的blob文件名会让路由直接报错。
3.4 Blob URL 的下载与内存回收:千万别忘了 revokeObjectURL
用URL.createObjectURL生成的 URL 会一直占着内存,直到页面关闭。如果连续生成几十个预览 URL 而从不释放,页面的内存占用会肉眼可见地飙升。规范做法是:用完之后立即调用URL.revokeObjectURL(url)释放。
const url = URL.createObjectURL(blob); // 下载场景:下载完成后释放 const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); // 图片预览场景:加载完再释放也可以,但如果之后还要用就暂不释放 img.src = url; img.onload = () => URL.revokeObjectURL(url);这里有一个微妙之处:在点击下载时浏览器可能还没有读取完 URL 指向的数据,立刻revokeObjectURL有概率导致下载失败。我常用的做法是setTimeout延迟 1 秒再释放,或者干脆在a.click()之后的宏任务里释放。预览场景可以在img.onload后再释放,因为图片已经加载到内存里了,释放 URL 不影响显示。
3.5 纯前端生成并导出 CSV:带 BOM 防中文乱码
这是后台管理里的高频需求,我给出一个可直接依赖的导出函数:
const exportCsv = (headers, rows, filename = 'export.csv') => { const escapeCell = (value) => { const str = String(value ?? ''); // 包含逗号、双引号、换行时需要转义,避免破坏 CSV 结构 if (str.includes(',') || str.includes('"') || str.includes('\n')) { return `"${str.replace(/"/g, '""')}"`; } return str; }; const lines = [ headers.join(','), ...rows.map(row => row.map(escapeCell).join(',')) ]; // 加 BOM 解决 Excel 中文乱码 const content = '\uFEFF' + lines.join('\n'); const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); };这个函数里容易忽略的是escapeCell的转义逻辑。如果某个单元格内容本身就包含逗号,比如"北京, 上海",不转义的话整行会被错误拆成多个单元格。HTML 里的表格可以随便放逗号,但 CSV 不行,这是纯文本格式的硬性规则,不处理就等着数据错位。
4. Blob 的底层细节和兼容性:踩坑经验汇总
4.1 type 属性与 MIME 类型:设置错了文件真的打不开
Blob 的type属性虽然只是“标签”,不会改变底层数据,但浏览器和操作系统会根据它来决定如何处理数据。举个例子:
// 错误示范:数据明明是 PDF,却写成了 text/plain const blob = new Blob([pdfData], { type: 'text/plain' });下载这个文件之后,系统会认为它是一个文本文件,双击打开可能就是乱码,即使扩展名是.pdf也无力回天。常见文件的 MIME 类型建议直接背下来:
| 文件类型 | MIME |
|---|---|
| 纯文本 | text/plain |
| HTML | text/html |
| CSV | text/csv |
| JSON | application/json |
| application/pdf | |
| PNG 图片 | image/png |
| JPEG 图片 | image/jpeg |
| GIF 图片 | image/gif |
| WebP 图片 | image/webp |
| JavaScript | text/javascript |
| MP4 视频 | video/mp4 |
| MP3 音频 | audio/mpeg |
| ZIP 压缩包 | application/zip |
需要说明的是,type并不会阻止文件下载,但它决定 Content-Type 信息,也会影响浏览器的预览行为。比如type: 'application/pdf'的 Blob URL 在浏览器里打开时会直接展示 PDF 阅读器,而type: 'application/octet-stream'则大概率触发下载。做在线预览功能时,这个细节格外重要。
4.2 size 属性与内存占用:你以为的轻量其实并不轻
Blob 虽然提供了“不复制数据”的便利,但它本身占用的内存依然和被包含的数据相关,尤其是手动new Blob([大字符串])的场景,字符串进入 Blob 时会按 UTF-8 编码成字节序列,内存占用至少是字符数的 1 到 4 倍。
隐藏得更深的是FileReader读完整文件、blob.text()转字符串、blob.arrayBuffer()转二进制,这些操作都会把整个 Blob 内容加载进内存。如果文件是 2GB,你就不应该用这些方法去“碰”它。正确的大文件处理姿势是:
- 用
blob.slice()切片操作,不去读取完整内容。 - 用
blob.stream()得到 ReadableStream,配合流式读取逐段处理。 - 若必须整体读取,则先做文件大小限制判断,超过阈值直接提示用户。
我踩过一次真实的坑:做一个多文件合并上传功能时,先readAsArrayBuffer读了一个近 1GB 的文件,页面直接内存溢出崩溃。后来改成边切片边上传,问题彻底消失。
4.3 新特性:blob.text()、blob.arrayBuffer()、blob.stream()
现代浏览器给 Blob 增加了一组更简洁的读取方法,比起FileReader回调而言,它们返回 Promise,和async/await配合非常舒服:
const blob = new Blob(['hello world'], { type: 'text/plain' }); // 读成文本 const text = await blob.text(); // 读成 ArrayBuffer const buffer = await blob.arrayBuffer(); // 拿 ReadableStream const stream = blob.stream(); const reader = stream.getReader();blob.stream()是处理大文件的利器,配合for await...of可以按 chunk 读取,不会一次性把所有数据塞进内存:
const reader = blob.stream().getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; console.log('读取到一段数据:', value); }不过这套新 API 的浏览器兼容性需要确认,尤其是企业项目里还要支持旧版浏览器的情况。我的建议是:常规场景直接用 Blob 自带方法,兼容性要求高时再退回FileReader。老平台的用户数量有时超乎想象,不能想当然只考虑最新浏览器。
4.4 Blob 与 Web Worker:把耗时操作挪到后台线程
前端处理大文件时,如果主线程长时间占用,页面会卡顿、动画掉帧、按钮点不动。把 Blob 读取和解析放到 Web Worker 里是绝对值得的优化。
由于 Worker 里的数据传递默认是结构化克隆,Blob 可以直接传给 Worker,不需要转成 JSON 或 ArrayBuffer:
// main.js const worker = new Worker('blob-worker.js'); worker.postMessage(blob); worker.onmessage = (e) => { console.log('worker 处理结果:', e.data); }; // blob-worker.js self.onmessage = async (e) => { const blob = e.data; const text = await blob.text(); // 做耗时解析 self.postMessage(text.length); };注意:虽然 Blob 可以结构化克隆传给 Worker,但这本质上是“引用传递”还是“数据复制”?现代浏览器对 Blob 的传递做了优化,不会直接复制底层数据,而是传递内部引用,所以开销很小。这一点实测下来很稳,处理几百 MB 的日志文件也不会让主线程卡死。
5. 常见问题与排查技巧实录
5.1 生成的 Object URL 内存泄漏怎么排查
Blob URL 泄漏是前端内存问题的常见来源,典型表现是页面长时间运行后内存持续走高。排查思路是按生成和释放是否成对来判断:
// 错误的写法:只 create 不 revoke function previewAll(files) { files.forEach(file => { const url = URL.createObjectURL(file); // 没有保存引用,也没有 revoke imgList.push({ url }); // 只是存了 url 字符串,无法释放 }); } // 正确的写法:save 和 release 成对 const urlMap = new Map(); function createPreview(file) { const url = URL.createObjectURL(file); urlMap.set(file, url); // key 用 file 对象 return url; } function releasePreview(file) { const url = urlMap.get(file); if (url) { URL.revokeObjectURL(url); urlMap.delete(file); } }实测经验:用Map以原文件对象为 key 记录 URL,组件卸载或预览关闭时统一释放,可以根治这个泄漏。不要迷信浏览器的自动回收,Blob URL 的生命周期和普通 DOM 引用不一样,没有自动释放机制。
5.2 CSP 和跨域对 Blob 有哪些影响
CSP(内容安全策略)里如果设置了img-src 'self',某些严格策略下blob:开头的图片也可能被拦。不同浏览器对blob:与 CSP 的组合处理不完全一致,最稳妥的方案是:在部署时给图片、媒体资源放宽到img-src 'self' blob: data:,并在开发环境就测一遍。
跨域方面:Blob 本身不受跨域限制,因为它不是一个可寻址的 URL 资源,而是浏览器内部数据。真正需要关注的是从fetch跨域接口拿到的数据,如果fetch成功但读取response.blob()报错,通常是响应体已经被消费,只能调用一次。遇到这种情况,改用response.clone()或者重新发请求。
5.3 大文件内存爆掉怎么办
最典型就是上面提到的读大文件直接崩溃。完整的防御姿势分三步:
- 在读取前检查文件大小,超过阈值直接拒绝。
- 大文件一律用
slice分片处理,不让整个内容进入内存。 - 数据解析用
stream逐段处理,避免一次性构建完整对象。
const MAX_SIZE = 200 * 1024 * 1024; // 200MB if (file.size > MAX_SIZE) { alert('文件过大,请使用分片上传或压缩文件'); return; }同时留意URL.createObjectURL和FileReader的叠加使用:如果同时 create 了多个大文件 URL,内存占用会叠加,用完就释放是最基本的职业素养。
5.4 移动端浏览器的兼容性坑
移动端对 Blob 的支持总体不错,但有几个实际差异值得注意:
- iOS Safari 对超大 Blob 的
slice()性能更差,分片大小建议适当调小到 1MB。 - 移动端
download属性在部分浏览器上不生效,Safari 里常常是直接打开文件而不是下载,需要额外配合navigator.share或提示用户长按保存。 - 老版本 Android WebView 对
new File()支持不好,尽量避免使用,改用 FormData 直接 append Blob。
实测下来,凡是要兼容老移动端浏览器的项目,我会准备一个“降级路径”:优先走URL.createObjectURL,拿到不行就退回 Base64 DataURL 方案,虽然体积变大但胜在兼容。
5.5 面试官常问的 Blob 问题与解题思路
Blob 在前端面试中出现频率很高,这几年我作为面试官也总结出几个必问题:
- Blob 和 ArrayBuffer 的区别是什么?回答思路:Blob 是高层级的不透明二进制容器,提供便捷切片和 MIME 类型;ArrayBuffer 是底层原始二进制缓冲区,需要 TypedArray 才能读写,可以按字节精准操作。
- 如何实现文件分片上传?回答思路:核心是
file.slice(start, end),配合 FormData 逐片上传,服务器按 index 合并,断点续传靠记录已传分片 index 实现。 URL.createObjectURL和FileReader.readAsDataURL有什么区别?回答思路:一个是生成临时对象 URL,零复制零编码;一个是把文件编码成 Base64 字符串,体积膨胀 33%。前者性能好但需要主动释放,后者兼容性强但占用大。- 为什么大文件上传不能直接用 FileReader 读完再传?回答思路:读完意味着整个文件进内存,内存会爆,且中途断线无法续传,切片是唯一可行路线。
把这些问题吃透,面试时不仅能答出概念,还能在现场写出blob.slice的代码,通过率会高很多。
在我的项目经历里,Blob 是从未缺席过的底层工具:大文件分片上传、图片本地预览、JSON/CSV 导出、音视频临时播放、SDK 日志上报,处处都是它的身影。单独记 API 确实容易忘,但只要你亲手做一次“分片上传 + 断点续传 + 纯前端导出”的完整实践,Blob 的用法就再也忘不掉了。最后再分享一个小技巧:尽量把 Blob 相关的工具函数封装成一个独立的fileUtils.js,项目里所有文件操作都走这一套封装,这样后续要加统一错误处理、加文件大小限制、做兼容性降级时,只需要改一个文件就够了。