前端文件处理核心:File、Blob、Base64 转换实战详解

📅 发布时间:2026/10/18 16:10:04
前端文件处理核心:File、Blob、Base64 转换实战详解
作为前端开发者文件上传、图片预览、下载导出这些需求几乎天天都能碰到。这里面的核心其实就围绕三个东西转File、Blob、Base64。如果你把这三个对象之间的关系理顺把互相转换的代码烂熟于心前端文件处理这块基本就没什么能难住你了。这篇内容适合刚接触文件处理的新人也适合想系统整理一遍知识体系、回头补坑的中级开发者。我们来把这几个 API 的底层逻辑和实战用法讲透。1. 文件处理的项目背景与整体设计思路1.1 前端文件处理每天都在解决什么问题文件处理并不是一个独立的页面功能它几乎无处不在。我拆一下最常见的场景上传用户选择本地文件前端需要先读出文件内容才能决定是直接丢给后端还是做前置校验、压缩、转码。预览用户还没点上传按钮就想先看看图片效果、视频是否正常这时候就需要前端自己生成一个临时预览地址。下载前端生成一份报表、一段 JSON、一个 CSV或者把多个图片打包需要直接触发浏览器下载不经过服务端中转。数据交换Base64 格式非常适合在 JSON 接口里传小体积的二进制信息比如头像、签名、单张压缩图片。跨端兼容不同浏览器对 File、Blob、ArrayBuffer 的实现细节有差异前端要统一转换成产品需要的格式。这些需求最终都指向一个核心问题二进制数据在前端怎么表示、怎么流转、怎么落地。File 是用户选择的文件Blob 是纯二进制数据块Base64 是一种可传输的文本编码方式。它们不是三个孤立的类型而是一条转换链路的两端和中间层。1.2 方案选型为什么是 File、Blob、Base64我在很多项目里见过两种极端做法。一种是什么都转成 Base64 拿去打接口小图片还好大文件直接卡死另一种是拿到 File 对象不知怎么处理只会上传原生表单遇到图片压缩、PDF 预览、Excel 导出就只能去后端写接口。两者的共同问题是没搞清楚三种类型的定位。先看三者的本质类型本质典型来源典型用途File浏览器中的文件对象包含文件名、大小、类型等元信息input[typefile]、拖拽事件、复制粘贴上传前的原始文件载体Blob纯二进制数据块可以看作文件内容的底层容器Fetch 响应、Canvas 输出、File 切片下载、展示、上传、分片Base64一种将二进制编码成 ASCII 字符串的文本格式FileReader 读取结果、Canvas 输出接口传输小文件、内嵌图片显示File 继承自 Blob它就是一个带有 name、lastModified 等额外属性的 Blob。所以凡是能用 Blob 的地方File 基本都能用凡是能接收 Blob 的 API也能接收 File。而 Base64 则是把二进制变成了纯文本方便放进 JSON、表单字段或者 img 的 src 里。设计上的选择依据很简单能用二进制对象Blob/File流转就尽量别转 Base64必须要文本传输才转 Base64。二进制对象体积小处理效率高不会因为字符串拼接造成额外内存开销Base64 体积约膨胀 33%在浏览器内存里还会以字符串形式额外存一份大文件场景很容易踩内存红线。1.3 转换链路一览前面说了选择依据现在把整个转换体系画出来讲比单看 API 更直观。前端文件处理的流转关系可以归纳成这几条链路文件源input/拖拽/粘贴 - File 对象File/Blob - Base64用 FileReader.readAsDataURL 或 Blob.arrayBuffer 编码Base64 - Blob/File把 Base64 字符串解码成 Uint8Array再 new Blob()Blob/File - 可预览的 URL用 URL.createObjectURL 生成 blob: 开头或 data: 开头的临时链接Fetch 响应 - Blob使用 res.blob()Canvas 画布 - Blob/Base64用 canvas.toBlob / canvas.toDataURL这七条链路基本覆盖了日常 90% 的文件处理需求。我在后面会逐条给出可复用的代码并解释每一步为什么要这么写。1.4 为什么前端处理方案能帮后端减轻负担很多人写文件上传都是直接把 File 丢给 FormData 就结束了。其实很多校验和加工工作完全可以在前端完成。比如图片上传前先验证尺寸和大小再把大图压到几百 KB把 EXIF 信息剥除用户上传体验会好很多后端也不必存储原图巨量数据。这种前置处理的价值不是“炫技”而是减少网络传输体积、缩短用户等待时间、降低服务端存储与带宽成本。后面 3.4 我会给出一套结合 Canvas 与 Blob 的图片压缩实现尽量把这类场景讲透让读者能直接抄作业又能理解改动逻辑。2. File 与 Blob 的核心细节解析2.1 File 与 Blob 的继承关系决定了很多 API 可以通用先说一个容易搞混的点。Blob 是最底层的二进制数据容器它不关心内容是什么只关心数据块。File 是 Blob 的子类在 Blob 的基础上增加了 name、lastModified 等文件元信息。因为继承关系存在你完全可以用 File 对象调用 Blob 的所有方法比如 slice() 切分文件片段、arrayBuffer() 读取完整内容。举例来说如果要实现大文件分片上传你只需要这样const file input.files[0] const chunkSize 2 * 1024 * 1024 // 每片 2MB const chunks [] for (let i 0; i file.size; i chunkSize) { chunks.push(file.slice(i, i chunkSize)) }这里的 file.slice 实际上调的是 Blob.prototype.slice返回的每个片段依然是 Blob 对象。你把这些 Blob 塞进 FormData 上传后端拿到的是一个个可以合并的文件块全程不需要读取完整内容进内存。理解这个继承关系后很多“为什么 File 也能调用某个 Blob 方法”的疑问就消失了。另外注意new File() 现在也是原生支持的可以由 Blob/ArrayBuffer 构造出 File 对象这在把 Base64 转回文件、或者 Canvas 导出后重新命名上传时非常有用。2.2 内存模型为什么大文件优先考虑 Blob而不是 Base64很多开发者没意识到一个关键点Base64 本质是字符串JavaScript 字符串在内存里是按 UTF-16 编码存储的处理过程中占用的空间往往比原始二进制大得多。一个 10MB 的文件转成 Base64 后字符数约为 10 * 4/3 ≈ 13.33MB但字符串在内存中按 UTF-16 存储时又可能翻倍。所以大文件一旦转 Base64浏览器内存占用会明显窜升页面卡顿甚至崩溃都是真实会发生的。Blob 则不同它在多数浏览器里指向一块不可变的原始二进制数据新建时不复制底层数据能够以较省内存的方式引用文件内容。所以做展示、下载、分片这些高频操作时第一选择是 Blob 和 Object URL而不是读成 Base64。只有当确实需要把数据通过文本形式交给后端、放到 img src 里或存到 LocalStorage 时再考虑转 Base64。2.3 FileReader 的三种读取结果怎么选FileReader 是一个经典 API它提供了多种读取方式关键是怎么选方法返回结果适用场景readAsDataURLBase64 格式的 data URL可直接放 img src图片预览、小图片接口传输readAsArrayBufferArrayBuffer真正的二进制缓冲区需要自己解析内容、加解密、自定义编码readAsText文本内容读取 txt、JSON、CSV 等内容FileReader 使用时要监听 onload 事件代码写起来偏回调风格。现代浏览器还有 File/Blob 的 arrayBuffer() 方法配合 async/await 写起来更舒服。不过 readAsDataURL 依然没有对应的便捷 Promise 版原生实现实际项目中我会封装一个小函数来读 Base64function fileToDataUrl(file) { return new Promise((resolve, reject) { const reader new FileReader() reader.onload (e) resolve(e.target.result) reader.onerror (e) reject(e.target.error) reader.readAsDataURL(file) }) }这个函数是很多上传预览组件的基础。注意上面使用了 FileReader 的 readAsDataURL返回的 result 是包含 MIME 前缀和 Base64 内容的完整 data URL例如data:image/png;base64,iVBORw0KGgo...。2.4 借助 fetch 把远端资源转 Blob 的妙用有些需求需要把网络上的图片或文件先在前端二次处理。这时候可以把请求结果转成 Blobconst response await fetch(https://example.com/poster.jpg) const blob await response.blob()这里的 blob 就拿到了原始的二进制。你可以继续把它转成 File、塞进 FormData也可以用 URL.createObjectURL(blob) 生成临时链接做预览甚至可以读成 Base64 后交给 Canvas 做压缩。这条 fetch - blob 的链路常被忽略但特别有用比如跨域图片水印、海报合成都离不了它。3. 实操过程从 File 到 Base64、从 Base64 到 Blob、从 Blob 到 URL 的完整实现3.1 上传前预览怎样把 File 转成 Base64 图片地址图片预览是最常见的需求。传统方案就是用 FileReader.readAsDataURL得到的 Base64 直接用 img 展示const file input.files[0] if (!file || !file.type.startsWith(image/)) { alert(请选择图片文件) return } const dataUrl await fileToDataUrl(file) document.querySelector(#preview).src dataUrl注意 type 校验很重要。很多旧项目不校验用户传个 Word 上去img 显示空白还找不到原因。文件大小也需要提前限制比如超过 10MB 直接拦截因为把 10MB 的图片转成 Base64 塞进 img src内存和渲染开销对整个页面都有影响。我还建议限制文件大小后再决定策略小于 2MB 用 Base64 预览就好更大的图片推荐用 URL.createObjectURL 生成临时地址预览或者直接在后端做缩略图。两者对比Base64 的优点是复制地址可以独立使用缺点是内存和体积Object URL 的优点是不增加多少内存、渲染快缺点是地址只在当前会话有效不能持久保存。3.2 Base64 怎么转回 Blob 和 File这是反向链条常见于后端返回一段 Base64 数据、Canvas 生成的是 Base64、或者接口加密传输、Web Worker 处理完毕回传 Base64此时前端要把它转回二进制文件再传给下一个环节。一个实用的转换函数function base64ToBlob(base64, contentType application/octet-stream) { const byteCharacters atob(base64.split(,)[1] || base64) const byteNumbers new Array(byteCharacters.length) for (let i 0; i byteCharacters.length; i) { byteNumbers[i] byteCharacters.charCodeAt(i) } const byteArray new Uint8Array(byteNumbers) return new Blob([byteArray], { type: contentType }) }如果 Base64 字符串带 data URL 前缀比如data:image/png;base64,...上面的 split 会去掉前缀只保留真正的 Base64 部分。得到 Blob 后想变成 File也很简单const file new File([blob], filename.png, { type: blob.type })这是我常用的手法先把 Base64 转成 Blob需要上传时再包装成 File。直接 new File 的好处是可以指定文件名上传接口里的 field 就能自动带上合理的文件名避免后端拿到一个乱码或没有名字的流。3.3 Blob 与 File 如何一键触发浏览器下载下载文件的需求也分两类。一类是后端返回了一个 Blob 流前端需要触发浏览器保存另一类纯粹是前端生成内容比如导出一个 CSV、JSON、文本。核心思路都是用 Blob 加临时 URL。写出一个通用下载函数function downloadBlob(blob, filename) { const url URL.createObjectURL(blob) const link document.createElement(a) link.href url link.download filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) }注意几个细节。第一a 标签的 download 属性在同源和 blob: 链接下生效跨域资源不一定能修改下载名。第二调用 revokeObjectURL 的时机要放在 click 之后不能太早释放否则某些浏览器会丢失下载但因为 click 是同步动作通常写成这样没问题。在 Safari 上偶尔会有兼容问题一些实践建议把 a 标签真正加入 document 中而不是挂空节点上面代码已经处理了这一点。纯前端导出 JSON 示例const data { name: test, list: [1, 2, 3] } const blob new Blob([JSON.stringify(data, null, 2)], { type: application/json }) downloadBlob(blob, data.json)如果是导出 CSV注意加 BOM。不留意的话中文在 Excel 里可能显示乱码const csvContent \uFEFF rows.map(r r.join(,)).join(\n) const blob new Blob([csvContent], { type: text/csv;charsetutf-8 })这个 BOM 细节我在项目里踩过很多次很多人导出字段内容含中文直接乱码加上 \uFEFF 后基本能解决。3.4 图片压缩Blob、Base64、Canvas 组合拳图片压缩是前端文件处理中最常见的进阶功能。我先给出一版通用实现。思路分四步读取原始文件并用 URL.createObjectURL 生成预览也可用 Base64。创建 image 对象拿到原始宽高。计算压缩后的目标尺寸绘制到 canvas。从 canvas.toBlob 直接拿 Blob或者 toDataURL 拿 Base64。核心代码async function compressImage(file, maxWidth 1200, quality 0.8) { const bitmap await createImageBitmap(file) const ratio Math.min(1, maxWidth / bitmap.width) const width Math.round(bitmap.width * ratio) const height Math.round(bitmap.height * ratio) const canvas document.createElement(canvas) canvas.width width canvas.height height const ctx canvas.getContext(2d) ctx.drawImage(bitmap, 0, 0, width, height) const blob await new Promise(resolve canvas.toBlob(resolve, image/jpeg, quality)) const compressedFile new File([blob], file.name.replace(/\.\w$/, .jpg), { type: image/jpeg }) return compressedFile }这里用了 createImageBitmap它比 new Image() 配合 onload 更现代能直接用 File 创建位图减少一次临时图片加载。如果担心兼容性也可以用 Image 对象 objectURL 的老组合效果类似。需要注意的是Canvas 绘制后通常会丢失 EXIF 信息所以照片方向可能翻转。优化处理要看 4.4 的说明。另外 toBlob 是异步回调如果项目需要大量并行压缩请控制并发数量别一次丢 20 张图不然浏览器会直接罢工。我实际做过一批几百张图的压缩工具核心就是控制并发在 3~5 个内存还能稳住。3.5 拖拽上传、粘贴上传怎样避免重复轮子文件处理不只有 input。拖拽上传和粘贴图片也很常见。拖拽上传的关键是从 DataTransfer 对象里取 filesdropZone.addEventListener(dragover, e { e.preventDefault() }) dropZone.addEventListener(drop, e { e.preventDefault() const files e.dataTransfer.files for (const file of files) { // 处理每个文件 } })粘贴上传一般用在富文本编辑器、IM 输入框监听 paste 事件editor.addEventListener(paste, e { const items e.clipboardData.items for (const item of items) { if (item.kind file) { const file item.getAsFile() // 处理粘贴的文件 } } })这两块代码看起来十分简单但配合前面的 Blob/Base64 转换就能组成完整的上传组件拖拽或粘贴拿到的 File接着压缩、校验、预览、分片或 Base64 传输。传参细节上记得 e.preventDefault()不然拖拽文件时浏览器会直接打开文件页面这个坑非常低级但几乎人人都踩过。3.6 结合 FormData 的完整上传封装把文件转成二进制后最终大多要交给 FormData 统一上传。这里给一个封装思路async function uploadFile(file, url, onProgress) { const formData new FormData() formData.append(file, file) const response await fetch(url, { method: POST, body: formData, }) if (!response.ok) { throw new Error(上传失败) } return response.json() }这个封装很基础但值得留意的是不要手动设置 Content-Type 为 multipart/form-data浏览器会根据 FormData 自动生成 boundary。手动设置恰恰是很多人的第一个大坑容易导致后端解析不出文件。上传进度可以用 XMLHttpRequest 的 upload.onprogress 实现fetch 目前没有原生的上传进度大文件场景还得用 XHR。4. 常见问题与排查技巧实录4.1 大文件转 Base64 卡死怎么办现象用户选了一个 50MB 的视频转 Base64 后整个页面卡了十几秒最后直接崩溃。原因就是 2.2 里说的内存模型。解决办法有三个方向一是限制前端能处理的最大文件体积比如超过 20MB 直接提示走原生上传二是大文件一律走 Blob 与分片不转 Base64三是需要预览大图时使用 URL.createObjectURL需要上传时使用分片上传或直传对象存储。顺带说一个优化细节上传前用 File.slice 分片每片 2MB配合并发上传既避免内存爆掉也能显示进度百分比。合理并发数在 3~6 个之间根据网络情况调。4.2 Base64 字符串过长导致接口报错或超时Base64 会让体积涨约 33%如果还带着 data URL 前缀一个几 MB 的图片转出来就是几十万字符。把它塞进普通 JSON 接口服务端经常会出现请求体过大、超时、解析慢的问题。我的建议是Base64 只适合小文件一般限制 1~2MB 以内。超过这个量用 FormData 或二进制直传。如果接口设计必须用 Base64需要与服务端沟通调整请求体大小限制同时前端做好 loading 和超时处理。4.3 由 canvas.toDataURL 转出的图片怎么上传更稳canvas.toDataURL 输出的是 data URL可以直接转 Base64 数据后再转 Blob 上传。一个常见路径是canvas.toBlob(async (blob) { // 这里可以直接 append 到 FormData无需再转 Base64 formData.append(file, blob, compressed.jpg) }, image/jpeg, 0.8)如果你仍然拿到的是 toDataURL 的字符串就用 3.2 的 base64ToBlob 转换。不过一般情况下能用 toBlob 就别用 toDataURL少一次字符串转换就少一层内存压力。另外canvas 转出来的 PNG 通常不小产品要求不高时可以强制 JPEG 并设置 quality压缩率会明显更好。4.4 图片压缩后的方向问题与 EXIF 丢失照片上传后有时会发现方向不对竖着拍的结果横过来了。原因是很多手机照片在 EXIF 里存了 Orientation 方向信息Canvas 绘制时如果不处理默认会忽略这个信息。解决方法首选在使用 createImageBitmap 时传入 orientation 处理参数。现代浏览器支持const bitmap await createImageBitmap(file, { imageOrientation: from-image })createImageBitmap 的 imageOrientation 选项能自动按 EXIF 方向调整这是很省心的做法。如果不支持就需要引入一个 EXIF 解析库读取 Orientation 后相应地旋转 canvas。这个知识点特别容易被忽略但偏偏是图片处理需求里最常见的失败场景。我自己处理的方案就是优先用 createImageBitmap兼容性不足时再降级到 Image 手动旋转。4.5 Blob URL 没有及时释放导致内存泄漏URL.createObjectURL 生成的临时 URL如果不调用 revokeObjectURL 释放会在浏览器里一直占着引用。大量预览、切片、下载操作如果一直累加内存会慢慢涨上去。解决思路是在下载或预览完成后马上 revoke。在组件卸载、页面销毁时统一释放所有创建过的 URL。如果页面里需要同时展示很多预览图片优先考虑只保留最近 N 个 URL旧的及时撤销。这个泄漏很隐蔽不是一下子爆掉而是在长时间使用中逐步变差。我第一次排查项目内存问题时最后定位到的就是几十个没有释放的 objectURL把图片列表翻页几十次内存占用就明显上去了。4.6 中文文件名、特殊字符乱码怎么处理上传或下载时文件名常被折腾坏。下载场景我经常这么处理function downloadBlob(blob, filename) { const url URL.createObjectURL(blob) const link document.createElement(a) link.href url link.download filename document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) }如果后端返回的 Content-Disposition 里文件名编码复杂前端最好自己指定文件名。Blob 创建 CSV/Excel 时加 BOM上面已经说过了。还有一个容易忽略的点从 input 选择的 File 本身带来的文件名如果是中文直接传给某些后端可能正常但传到对象存储有时会出现 encode 问题可以考虑用 encodeURIComponent 统一处理后再传。4.7 浏览器兼容性与能力检测File、Blob、FileReader 这几个基础 API 在现代浏览器里非常稳但一些新 API 有兼容差异能力兼容情况备注createImageBitmapChrome/Edge/Firefox 支持Safari 部分版本支持需要降级到 Image 方案canvas.toBlob老 Safari 可能不支持可降级到 toDataURLFile 构造函数IE 不支持现代浏览器 OK可以 new Blob 手动包装fetch res.blob()现代浏览器 OKIE 无 fetch用 XMLHttpRequest 兼容URL.createObjectURL现代浏览器 OKIE 前缀写法已不再必要做工具函数时用能力检测配合降级是稳妥策略。比如 toBlob 不存在时用 toDataURL 转 Blob或者用 polyfill 补齐避免生产环境翻车。一个实用的小技巧如果需要对 File 做大量切片或并行处理可以在开头统一把 File 转成 Blob 再操作因为 Blob 类型更通用传进 FormData 或生成 objectURL 都没问题。如果业务上需要保留文件原始名称直接 new File([blob], name, { type }) 包装回去即可。5. 工具函数合集File、Blob、Base64 转换速查这里我把前文中一直在用的函数整理成一个可直接复用的速查集合。它们不仅是我在多个项目里提取出来的常见封装也兼顾了异常处理和易用性。5.1 Blob 与 File 互转工具function blobToFile(blob, fileName, mimeType blob.type) { return new File([blob], fileName, { type: mimeType }) } function fileToBlob(file) { return file.slice(0, file.size, file.type) }5.2 File/Blob 转 Base64function blobToDataUrl(blob) { return new Promise((resolve, reject) { const reader new FileReader() reader.onload () resolve(reader.result) reader.onerror () reject(reader.error) reader.readAsDataURL(blob) }) }File 是 Blob 子类所以同样可以用 blobToDataUrl(file)。5.3 Base64 转 Blob 和 Filefunction base64ToBlob(base64, contentType application/octet-stream) { const parts base64.split(,) const mime parts[0]?.match(/data:(.*?);base64/)?.[1] || contentType const raw atob(parts[1] || parts[0]) const bytes new Uint8Array(raw.length) for (let i 0; i raw.length; i) { bytes[i] raw.charCodeAt(i) } return new Blob([bytes], { type: mime }) } function base64ToFile(base64, fileName, contentType) { const blob base64ToBlob(base64, contentType) return new File([blob], fileName, { type: blob.type }) }注意 atob 的性能在超大字符串下会慢必要时可以用循环批处理优化。5.4 通用上传与下载速查上面已经多次出现 downloadBlob 和 uploadFile我把它们放在一起做成一个 mini library方便读者直接拉进自己的 utils。文件处理看着简单但一旦量大、场景杂就会发现关键不是记住了多少个 API而是能不能在设计阶段就想好数据流。6. 实际项目中的一点体会说点真实项目里的体会。我在一个模拟项目 X 中做过一套多格式导出工具用户可以选择导出图片、表格或压缩包。最初是按需写了很多分散的逻辑后来统一收拢成 File - Blob - Base64 / objectURL 的转换层再加一层 registry 记录 URL 并统一释放内存问题和文件混淆问题都明显减少了。另外代码里不要堆太多重复的转换函数。团队里每个成员都写自己的 fileToDataUrl风格不一致换人维护时很难受。建议统一沉淀成一个 utils 文件专门负责这三个对象之间的转换与边界处理大小限制、MIME 校验、URL 回收业务组件只调接口不碰底层细节。最后分享一个小技巧在为第三方接口或后端协作时如果预期文件可能比较大尽量在设计沟通阶段就确认好传输方式。是走 FormData 分片还是 Base64 单发会直接决定前端代码走向。宁可早点多讨论几轮也别等页面写完了再去改传输层。文件处理这块没有太多玄学把二进制链路理顺后面基本是一马平川。