资讯详情

前端 Blob 完全指南:从文件上传到分片下载的实战手册

📅 2026/10/9 12:46:40 | 华诺云谱 👁 阅读
前端 Blob 完全指南:从文件上传到分片下载的实战手册
我在实战里处理过太多文件上传、图片预览、报表导出的需求几乎每个项目都绕不开 Blob可不少前端同学一提到 Blob 就只说得出“它是一个二进制对象”真到了要处理分片上传、实现文件下载、排查内存泄漏的时候又完全无从下手。这里我把我对 Blob 的完整理解、实际项目中沉淀下来的用法和经验一次性讲清楚包含可直接抄的代码和踩坑记录希望能帮你真正把它变成自己的技能。Blob 是前端处理文件和数据流的基础能力涵盖文件上传、下载、预览、分片传输等多个高频场景适合从初级到进阶的所有前端开发者阅读读完你至少能独立完成大文件分片、纯前端导出文件、图片本地预览这些常见功能。1. Blob 到底是什么先撕开前端文件处理的第一层膜1.1 一句话解释它就是浏览器里的一堆原始字节Blob 的全称是 Binary Large Object翻译过来就是“二进制大对象”它在浏览器里代表一段不可变的原始二进制数据。你可以把它想象成一个麻袋你把文件内容、字符串、二进制数据一股脑扔进去它帮你完整地兜住之后你需要用的时候再从这个麻袋里往外拿。数据一旦被装进 Blob就不能直接在原对象上修改如果想改只能新建一个 Blob 实例。这里的关键词是“不可变”。很多人不理解为什么 Blob 要设计成不可变其实这是为了性能和安全考虑多个地方可以同时持有同一个 Blob 的引用而不用担心数据被意外改动浏览器内部也可以放心地做数据共享和缓存。Blob 最典型的来源有两个input[typefile]用户选择的文件会生成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 数据与原文件一致DataURLBase64 字符串小图片预览、Canvas 导出膨胀约 33%理解了这个层次关系你看到代码里File能调用 Blob 的slice()方法就不会觉得奇怪了因为子类天然继承父类的全部能力。1.3 Blob 的构造函数和内部结构直接看一段最基础的创建代码const blob new Blob([htmlhello/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[typefile]); 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()生成的数据最好显式地给它一个文件名做法是构造Fileconst 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;charsetutf-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); // 读取为 DataURLBase64 字符串 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;charsetutf-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/plainHTMLtext/htmlCSVtext/csvJSONapplication/jsonPDFapplication/pdfPNG 图片image/pngJPEG 图片image/jpegGIF 图片image/gifWebP 图片image/webpJavaScripttext/javascriptMP4 视频video/mp4MP3 音频audio/mpegZIP 压缩包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项目里所有文件操作都走这一套封装这样后续要加统一错误处理、加文件大小限制、做兼容性降级时只需要改一个文件就够了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑