前端文件处理利器:FileReader与Blob实战详解
1. 从一次文件上传说起为什么前端必须掌握FileReader和Blob前两周有个朋友跟我抱怨说他们公司做内部管理系统遇到一个特别头疼的需求用户要上传几百MB的压缩包还要能预览图片、查看文本文件内容甚至要把Excel里的数据抽出来做校验。他第一反应是这得走后端吧结果后端同事冷笑一声说文件解析本来就能在前端做你查查FileReader。其实这句话对了一半。FileReader确实能在浏览器本地读取文件内容但真正让前端文件操作从花了呼哨变成生产力工具的是FileReader和Blob这对组合。一个负责把File/Blob数据读进内存并转成可用的文本、DataURL或二进制buffer另一个负责用结构化方式描述、切分、组合二进制数据。两者互相配合才能完成前端文件上传预览、分片传输、本地导出、图片压缩这些日常刚需。这篇文章不打算讲那些官方文档里一抄一大段的API罗列。我从一个实际项目里踩过的坑出发把FileReader的四个读取方法、Blob的切片和应用场景、以及两者配合时最容易出问题的地方全部掰开揉碎讲清楚。适合刚接触前端文件处理的技术新人也适合想系统梳理一遍、避免在项目里踩坑的进阶开发者。先花一分钟理解一个基础概念浏览器里一切文件操作的本质都是二进制数据操作。FileReader是将二进制转为可读内容的翻译官Blob是二进制数据的容器和切分器。绕明白这两句话后面全是细节。2. FileReader把文件读进内存的四把钥匙FileReader说白了就是浏览器提供的一个文件内容读取器。你给它一个File对象或者Blob对象它通过内部异步机制把内容读出来然后通过事件回调把结果交给你。之所以叫Reader是因为它只管读,不管写——写文件、生成文件是另一码事稍后讲Blob时再聊。这里有个关键认知FileReader的所有读取方法都是异步的。也就是说你调用readAsText(file)之后文件内容不会立刻出现在返回值里而是要等onload事件触发才能拿到结果。新手最容易犯的错误就是刚调用完就立刻取result结果自然是undefined。2.1 readAsText读文本文件的最短路径readAsText(blob, encoding?)会把Blob内容按指定编码解析成字符串第二个参数不传时默认UTF-8。这个方法最适合读纯文本、JSON、XML、CSV这类数据。const input document.querySelector(input[typefile]); input.addEventListener(change, async (e) { const file e.target.files[0]; const reader new FileReader(); reader.onload () { console.log(reader.result); }; reader.readAsText(file, UTF-8); });看起来很简单但有几个细节值得注意。第一onload里拿到的reader.result是字符串。如果你读的是JSON文件记得JSON.parse(reader.result)因为FileReader不管内容结构它只负责把字节变成字符串。第二编码参数很重要。很多系统里用户会上传GBK编码的CSV或TXT这时不指定GBK或者GB18030中文一定会乱码。实测下来某些浏览器并不认GBK这个标签要写成GB18030才能兼容这是我踩过的第一个坑。第三超大文本文件慎用readAsText。一个100MB的TXT文件会被整体读成一个超长字符串内存占用翻倍还会卡死渲染线程。遇到大文件正确做法是先切片再用readAsArrayBuffer逐段处理。后面讲分片上传时我会给完整示例。2.2 readAsDataURL图片预览的规范姿势readAsDataURL把文件内容编码成Base64字符串并且自动加上MIME类型前缀。比如读一张PNG图片得到的结果长这样data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA...这种DataURL可以直接塞给img.src、a.href实现预览和下载。好处是简单直观不需要额外生成临时URL符合浏览器安全策略不需要考虑内存释放。坏处是Base64编码会让体积膨胀约33%而且一旦编码成字符串你拿到的就不再是原始二进制数据后续没法直接传给二进制上传接口。我个人的经验是图片预览优先用URL.createObjectURL而不是readAsDataURL两者我都后文对比。但如果图片本身要参与表单提交、跨端传输或需要暂存在本地比如存进localStorage做草稿箱DataURL反而是更好的选择。// 图片预览 - DataURL方案 const reader new FileReader(); reader.onload (e) { previewImg.src reader.result; // data:image/xxx;base64,.... }; reader.readAsDataURL(file);这里有一个非常隐蔽的坑用readAsDataURL读大图片时Base64字符串会非常长放在img.src里虽然能显示但内存压力很大而且浏览器地址栏或某些组件的日志里如果打了这个字符串页面直接卡死。我在一个后台管理系统里就遇到过用户上传一张8MB的照片console.log了预览字符串整个DevTools直接崩溃。2.3 readAsArrayBuffer接触二进制原貌的入口readAsArrayBuffer返回ArrayBuffer对象它是一段固定长度的原始二进制数据缓冲不包含任何字符串语义。你可以在后续用DataView或Uint8Array等TypedArray视图去解析、修改、读取其中的字节。reader.onload () { const buffer reader.result; // 这是ArrayBuffer const bytes new Uint8Array(buffer); // 按字节视图解读 console.log(bytes.length, bytes[0]); // 例如文件长度、第一字节 }; reader.readAsArrayBuffer(file);这个方法几乎是所有前端二进制处理的基础。判断文件真实类型魔数校验、读取图片宽高、解析文件头、计算MD5/SHA1哈希、分片上传统统需要先拿到ArrayBuffer再操作。举个实际例子图片的PNG格式前8个字节固定是89 50 4E 47 0D 0A 1A 0AJPEG前3字节是FF D8 FF。想在前端做文件类型校验不能只看input的accept属性因为用户改扩展名就能绕过。正确做法是读ArrayBuffer后检查魔数function checkIsPng(buffer) { const bytes new Uint8Array(buffer.slice(0, 8)); const pngSignature [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; return pngSignature.every((v, i) bytes[i] v); }这段代码依赖ArrayBuffer.prototype.slice注意它与Blob.slice的返回类型不同前者返回新的ArrayBuffer后者返回新的Blob新手很容易搞混。2.4 生命周期事件与结果缓存FileReader除了onload还有onerror、onabort、onloadstart、onloadend、onprogress几个常用事件。实际项目中至少需要处理onerror和onloadend。onloadend无论成功失败都会触发适合统一关闭loading状态。const reader new FileReader(); reader.onloadstart () { showLoading(); }; reader.onprogress (e) { if (e.lengthComputable) { const percent Math.round((e.loaded / e.total) * 100); updateProgress(percent); } }; reader.onload () { handleResult(reader.result); }; reader.onerror (e) { showError(读取失败); }; reader.onloadend () { hideLoading(); }; reader.readAsText(file);还有两个不可不知的细节。其一FileReader.result在读取成功后会一直保留在对象上不会自动清空。如果一个FileReader对象被复用去读多个文件新结果会覆盖旧结果但底层的旧内存并不会立刻释放。处理大量文件时建议每读一个文件就新建一个FileReader或者读完就reader null避免内存回收不及时。其二reader.abort()可以中断读取触发onabort事件。但在文件很大的情况下中断之后的内部状态有些浏览器处理得并不干净最稳妥的做法是直接用新实例替换旧实例而不是尝试复用。3. Blob二进制数据的乐高积木如果说FileReader是读文件内容那Blob就是描述和操作二进制数据本身。Blob的全称是Binary Large Object浏览器里它代表一个不可变的原始数据块。所有File对象实际上都继承自Blob——这点非常重要意味着几乎所有能操作Blob的API同样能操作File。3.1 Blob的构造与type属性的门道创建Blob有两种方式。一种是直接newconst blob new Blob([hello world], { type: text/plain }); const jsonBlob new Blob([JSON.stringify({a: 1})], { type: application/json });另一种是从已有Blob里切出一段const sliced largeBlob.slice(start, end, type);构造时传入的数组里可以是字符串、ArrayBuffer、TypedArray、DataView甚至另一个Blob。浏览器会把它们按顺序拼接成一个整体。type属性表示MIME类型这个字段的违规率非常高。很多人随手填一个application/octet-stream完事结果后续下载文件时扩展名不对、预览时找不到合适的渲染器。从实践角度讲图片image/png、image/jpeg、image/webp文本text/plain或text/markdownJSONapplication/json压缩包application/zip通用二进制application/octet-stream这个字段既是预览和下载的依据也参与了文件类型识别。虽然真实文件头的魔数才是权威但type字段如果填错了很多场景下会直接导致功能异常。我在项目中遇到过用Blob生成后端需要的JSON文件时type填了text/plain下载下来系统识别不了。改回application/json即刻解决。3.2 从Blob创建预览URL前面提到图片预览有两种思路这里全部展开。URL.createObjectURL(blob)**会根据Blob生成一段临时URL格式是blob:http://域名/uuid。它引用着内存里的Blob对象浏览器渲染图片、视频、PDF时可以直接加载这段URL。const objectUrl URL.createObjectURL(file); previewImg.src objectUrl;和DataURL相比objectURL有三个明显优势体积零膨胀不经过Base64编码性能开销极小生成速度快支持大文件预览保真性好视频、PDF等二进制格式也能直接预览但代价是这段URL的生命周期必须由你手动管理。一旦不再使用必须调用URL.revokeObjectURL(objectUrl)释放内存。很多项目里只create不revoke后果就是页面越用越卡尤其是上传预览图片频繁操作的页面内存直线往上涨。正确的姿势是在图片加载完成后尽快释放const img new Image(); img.onload () URL.revokeObjectURL(objectUrl); img.src objectUrl;需要注意的是revoke要放在onload回调里而不是调用create后立刻执行。revoke太早img还没来得及加载预览框变成一片空白。3.3 Blob切片与分片上传的实现思路Blob有一个极其实用的能力slice(start, end, contentType)可以切出原始数据的任意子段返回新的Blob。这个能力直接支撑起前端分片上传、断点续传、视频流加载等场景。先看基础分片代码const CHUNK_SIZE 2 * 1024 * 1024; // 每片2MB function sliceFile(file) { const chunks []; let start 0; while (start file.size) { const end Math.min(start CHUNK_SIZE, file.size); chunks.push(file.slice(start, end)); start end; } return chunks; }这个循环很基础真正有讲究的是切完之后的上传逻辑。我在项目里用过一个组合思路async function uploadChunks(file) { const chunks sliceFile(file); const total chunks.length; for (let i 0; i total; i) { const formData new FormData(); formData.append(chunk, chunks[i], file.name); formData.append(index, i.toString()); formData.append(total, total.toString()); formData.append(filename, file.name); // 用fetch逐个上传 await fetch(/api/upload, { method: POST, body: formData }); progress.innerHTML 已上传 ${i 1}/${total}; } }这段代码的实际效果是前端把大文件切成多个小Blob每个独立上传服务端接收后按index顺序合并。相比一次传整个文件好处有三点第一避免大文件单请求超时。很多代理服务和网关对请求体大小和时间限制得非常死2MB一个的请求几乎不会触发超时。第二支持并发控制。可以把循环改成Promise.all限制同时3~5个请求实测上传速度比串行快很多。第三具备断点续传的雏形。如果某个分片上传失败重新上传失败分片即可不需要整个文件重来。但这里有一个最容易遗漏的细节分片上传时Blob切片返回的是一个视图式的Blob它内部引用的是原文件部分字节不是复制出来的独立数据。也就是说原文件对象如果被垃圾回收切片切片可能出问题。因此分片期间必须保持原File对象引用不丢失。一些打包工具或浏览器优化可能会提前调整原Blob的生命周期所以最好把原文件也挂在作用域里或显式保留引用。3.4 用Blob实现前端文件导出与下载Blob第三大高频应用场景是前端生成文件并下载。比如数据导出到CSV后端不参与纯前端搞定。function exportCSV(data) { const rows data.map(row row.join(,)).join(\n); const blob new Blob([\uFEFF rows], { type: text/csv;charsetutf-8; }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download 导出数据.csv; a.click(); URL.revokeObjectURL(url); }这段代码里的\uFEFF是BOM头专门用来解决Excel打开CSV时中文乱码的问题。不加BOMWindows上的Excel大概率以ANSI编码解读UTF-8的CSV中文直接变乱码。加完BOMExcel才会正确识别为UTF-8。另一个细节是a.click()之后要立刻revoke吗实践中最好用setTimeout延迟到点击事件完成后释放或者干脆在下载事件响应后释放。有些浏览器在点击事件同步执行完之前释放URL会导致下载失败。稳妥写法a.onclick () setTimeout(() URL.revokeObjectURL(url), 1000);4. 工具选型与平台差异这些坑不踩等于白学FileReader和Blob虽说是浏览器原生能力但在真正的项目中必须考虑兼容性、性能边界、以及和现有框架的配合。4.1 兼容性现状一个残酷的事实FileReader在IE10才完整支持Blob和URL.createObjectURL更是要IE10。如果项目还要兼容IE9及以下这两个API基本没法用只能借助后端或者Flash方案——不过现在这么老的浏览器几乎已经绝迹这里只提一句考古。现代浏览器中两者的实现趋于一致但仍有微妙的差异new Blob()的构造函数在Safari低版本上不支持需要PolyfillFileReader的readAsBinaryString被标记废弃不要使用URL.createObjectURL在部分Android WebView上生成的URL过期时间不稳定必要时改用DataURL兜底我在一个混合开发项目里遇到过iOS的WKWebView对大Blob的objectURL支持很好但Android的X5内核下URL.revokeObjectURL执行时机稍晚就会导致内存泄漏。最终的处理方案是统一用FileReader.readAsDataURL做图片预览虽然性能略差但平台一致性更好。这个决策的依据是项目里图片体积不大几乎都在2MB以内性能损耗在可接受范围内而平台一致性直接决定了用户体感。4.2 性能边界与内存管理FileReader和Blob的配合最容易出性能问题的是大文件全量读进内存。比如一个200MB的压缩包你调用readAsArrayBuffer想算MD5瞬间浏览器内存上涨几百MB。不是不能做而是要控制并发、控制单次读取量。我在实际项目中总结了一套经验文件小于5MB直接readAsArrayBuffer读全量无脑简单文件5MB~50MB先切片每片2MB逐片读取并计算hash或上传文件大于50MB必须结合file.slice分片且每读完一片手动把对应的Blob引用置空另外一个被忽视的细节是FileReader读大文件时result里的ArrayBuffer在读取结束后不会被自动释放即使reader null也只是断掉了引用。如果想强制回收内存可以把reader.result也置空或主动调用reader.abort()后再置空。实测在Chrome里reader null; reader.result null;之后堆内存会明显下降。4.3 FileReader与Blob对象的关系速查很多文档把FileReader和Blob混在一起讲导致新手搞不清谁是谁。我用一个小表格整理能力FileReaderBlob读取文件内容为文本支持readAsText不支持读取文件为DataURL支持readAsDataURL不支持读取为ArrayBuffer支持readAsArrayBuffer不支持生成临时预览URL不支持支持URL.createObjectURL切片不支持支持blob.slice构造新文件不支持支持new Blob与FormData配合不能直接appendFile本身就是Blob子类可直接append这表格我背了很久。只要记住这两句话FileReader负责从Blob/File里把字节读出来Blob负责组织和切割数据本身。当然File也继承了Blob的一切能力所以file.slice能切file.type能查URL.createObjectURL(file)也能用。5. 实操环节一个文件预览与分片上传的综合Demo讲了这么多理论必须上点能直接抄的代码。我拿一个典型的管理后台文件管理系统场景举例完成三件事图片本地预览、文本文件内容读取、大文件分片上传。这三件事分别对应FileReader的两个方法和Blob切片。5.1 界面与交互设计页面上有一个文件选择框支持多选可以识别的文件类型包括图片、文本、CSV。选择后页面分为三个区域图片预览区用URL.createObjectURL展示缩略图文本内容区用readAsText读取并显示文本内容前1000个字符上传进度区无论什么文件都走分片上传显示进度条这个设计瞄准的是实际需求预览是给用户即时反馈的内容读取是为了前端本地校验比如检查CSV表头格式是否正确分片上传是为了服务端接收大文件时更稳定。5.2 核心代码实现完整代码贴出来可以直接放在一个HTML文件里跑!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleFileReader与Blob综合实操/title /head body input typefile idfileInput multiple / h3图片预览/h3 div idpreview/div h3文本内容/h3 pre idtextContent/pre h3上传进度/h3 div idprogress/div script const input document.getElementById(fileInput); const preview document.getElementById(preview); const textContent document.getElementById(textContent); const progress document.getElementById(progress); input.addEventListener(change, (e) { const files Array.from(e.target.files); files.forEach(handleFile); }); function handleFile(file) { // 根据type分流处理 if (file.type.startsWith(image/)) { previewImage(file); } else if (file.type.startsWith(text/) || file.type.includes(json) || file.type.includes(csv)) { readTextPreview(file); } uploadChunks(file); } function previewImage(file) { const url URL.createObjectURL(file); const img document.createElement(img); img.src url; img.style.maxWidth 200px; img.style.margin 8px; img.onload () URL.revokeObjectURL(url); preview.appendChild(img); } function readTextPreview(file) { const reader new FileReader(); reader.onload () { const str reader.result; // 只预览前1000字符避免大文本卡页面 textContent.textContent str.slice(0, 1000); // 检测是否JSON并尝试格式化 if (file.type.includes(json)) { try { const obj JSON.parse(str); textContent.textContent JSON.stringify(obj, null, 2); } catch (err) { textContent.textContent 解析JSON失败 err.message; } } }; reader.onerror () { textContent.textContent 文件读取失败; }; reader.readAsText(file, UTF-8); } const CHUNK_SIZE 1 * 1024 * 1024; // 1MB一片 function sliceFile(file) { const chunks []; for (let start 0; start file.size; start CHUNK_SIZE) { const end Math.min(start CHUNK_SIZE, file.size); const chunk file.slice(start, end); chunks.push(chunk); } return chunks; } async function uploadChunks(file) { const chunks sliceFile(file); const total chunks.length; for (let i 0; i total; i) { // 模拟上传实际上传到后端接口 await simulateUpload(chunks[i], i, total, file.name); progress.textContent 文件 ${file.name}${i 1}/${total} 片; } } function simulateUpload(chunk, index, total, filename) { return new Promise((resolve) { setTimeout(() { console.log(第${index 1}/${total}片上传完成大小${chunk.size}字节); resolve(); }, 200); }); } /script /body /html三个模块的代码都不复杂但组合起来就能覆盖日常大部分文件处理需求。这个Demo我用在一个内部的需求文档上传中心项目里当时配合后端把接口对接好全流程跑得还算顺利。5.3 实操中发现的关键问题第一个问题是input的multiple属性配合文件校验时用户可能一次选多种类型文件导致预览区卡片混排。我后来加了分组渲染每种类型一个卡片组UI更清晰。第二个问题是文本预览的编码问题。浏览器通过file.type判断文本类型是靠扩展名和后端返回的MIME但CSV文件的编码经常是GBKreadAsText(file, UTF-8)读到一半乱码。我后来加了一个降级策略如果检测到乱码字符\uFFFD就用TextDecoder(gb18030)重新解码ArrayBuffer。这个坑在实际业务里太常见了。function decodeText(buffer, fallbackEncoding) { try { return new TextDecoder(fallbackEncoding).decode(buffer); } catch (e) { return new TextDecoder(utf-8).decode(buffer); } }第三个问题是分片上传时Excel等二进制文件其实没必要去预览但业务要求选中文件后立即反馈。所以我在上传前会先显示文件大小和类型元信息再异步做预览和读取避免阻塞流程。6. 进阶FileReader和Blob与现代框架如何共存现在的项目基本都用了React、Vue或小程序框架。FileReader和Blob本质上属于浏览器原生能力框架层面没有特殊适配但组合使用时依然有几个值得留意的配合点。6.1 在React/Vue中的状态管理Blob对象和FileReader实例都不是React状态也不应该放进state里。因为Blob可能很大放进去会造成不必要的渲染和内存拷贝。正确做法是File对象放在ref里引用读取结果只把字符串或ArrayBuffer放入state。我在React项目里写过类似这种代码const [text, setText] useState(); const fileRef useRef(null); const handleChange (e) { fileRef.current e.target.files[0]; const reader new FileReader(); reader.onload () setText(reader.result); reader.readAsText(fileRef.current, UTF-8); };这个细节看似简单实际很多人把File对象直接setState然后发现输入框的值还是空的因为File对象的引用变了导致内部状态混乱。保持File对象引用不变结果字符串走state是最稳的配合姿势。在Vue3的setup里同理用shallowRef或者普通ref都可以但注意不要在computed里直接引用Blob对象做派生计算——每次依赖变化都会重新读文件性能极差。6.2 小程序端的局限性再提一句小程序。小程序里没有标准FileReader和Blob但微信小程序提供了wx.getFileSystemManager().readFile支付宝小程序有自己的my.readFile。整体能力对标FileReader但细节差异很大尤其是编码和路径处理。如果你的团队同时做Web端和小程序端建议封装一层统一的文件读取接口Web端用FileReader小程序端用各自API上层业务逻辑保持一致。我参与过一个跨端项目最初直接在小程序里用Web的思维写文件处理结果到处踩坑小程序读取的是本地临时文件路径不是File对象读取JSON必须指定encoding为utf8读取ArrayBuffer后要转字符串需要ArrayBuffer → Uint8Array → String.fromCharCode的方法链。后来统一封装成async readText(path)和async readArrayBuffer(path)两个方法才把两端的复杂度隔离掉。6.3 与装载器的配合再说一个容易被忽略的场景FileReader读取的文件如果最终要传给后端用FormData时不需要自己读内容直接formData.append(file, file)就行。只有要读取内容做预览或校验时才需要FileReader。很多新手把上传文件和读取文件混为一谈以为不上传就不需要FileReader或者以为上传前必须先读取。其实上传是底层协议的事浏览器会自己读文件流前端只需要把File对象交给FormData或fetch的Body即可。理解这个边界之后代码写起来会清晰非常多。FileReader用于我要看、我要算、我要改、我要校验的场景Blob用于我要切、我要拼、我要临时URL、我要生成文件的场景。两者合起来前端文件操作的全拼图才完整。我在实际项目中还发现一个隐藏技巧用FileReader.readAsArrayBuffer读取文件后配合CryptoJS或浏览器内置的crypto.subtle.digest计算文件SHA256在前端做文件去重、秒传判定完全可行。这块如果大家感兴趣我后续可以单独写一篇前端计算大文件哈希的工程化方案。7. 常见问题排查与避坑速查表最后把这段时间实战中高频遇到的坑集中列出来给读者当速查手册。这些问题每一个我都实际碰过有些甚至排查了整整一天。7.1 高频报错与解决方案报错/现象根因解决方案reader.result为null读取还没完成就取值等onload触发后再取读取JSON中文乱码编码指定错误或源文件非UTF-8readAsText时指定GB18030或用TextDecoder降级URL.createObjectURL后图片不显示revoke时机太早在img.onload后再revoke大文件上传浏览器崩溃全量读取ArrayBuffer分片、限制每次读取量Blob转File时lastModified丢失未显式赋值new File([blob], name, {lastModified})分片上传后文件合并损坏切片时end计算错误使用Math.min(startsize, file.size)readAsDataURL内存暴涨Base64膨胀33%改用objectURLSafari下new Blob报错低版本不支持构造参数引入Blob Polyfill或改用File对象7.2 实战排错实录有一次线上反馈用户上传的CSV文件在预览时前几行正常后面全乱码。排查过程是这样的第一步怀疑编码但全文件前几行正常后面乱不太像整体编码问题。第二步怀疑文件太大但文件只有3MB不至于卡。第三步直接用ArrayBuffer读出来发现乱码集中在某一行之后。仔细对比发现这一行恰好是某个字段里出现了一个特殊字符。最终定位文件里包含的某个特殊字符被浏览器判定为二进制数据导致FileReader的文本解码在那一行中断。解决方法是改用readAsArrayBuffer读取全量再用TextDecoder异步分块解码。这种问题单纯看文档根本发现不了必须动手读字节才能定位。还有一次遇到预览PDF时白屏。排查发现URL.createObjectURL(blob)在Safari下能生成URL但WebView的PDF预览组件不支持blob协议。最终方案是改用readAsDataURL生成DataURL递给PDF预览组件同时通过window.open打开新窗口预览。这类兼容性适配经验只能靠踩坑积累。7.3 一些写在代码注释里的经验最后分享几条我每次写文件处理代码时都会遵守的约定所有FileReader实例用完后置空引用尤其循环读取时创建ObjectURL后必须配套revoke可以用try/finally保证文本读取优先使用TextDecoder而非readAsText因为前者支持分块解码和按需结束字符串文件大小校验放在所有读取动作之前避免内存浪费处理超大文件时用requestIdleCallback分批处理Blob切片避免长时间占用主线程这些约定看着琐碎但长期坚持下来项目的稳定性提升非常明显。我见过太多功能能跑但一到大文件就崩的代码基本都是在这几个细节上偷懒导致的。文件操作虽然只是前端的一小部分但FileReader和Blob这对组合掌握好之后前面那些只能靠后端的需求会少一大半。下次再遇到文件预览、分片上传、本地导出别急着向后端求助先在浏览器里把数据的来龙去脉摸清楚。