很多年前我第一次接手一个内部系统改造甲方抛过来一个需求浏览器里要能传好几个 GB 的工程文件。我一开始想的很简单form 里塞个 input后端接住流不就行了。结果真去验证才发现别说几个 GB超过 2GB 的文件浏览器直接把请求给你掐了后端也是各种超时上传速度感人页面卡顿用户体验基本归零。后来老老实实上了分片上传方案才算是把这件事彻底解决。这篇文章就把我落地这套方案时用到的核心代码、踩过的坑、以及设计思路都摊开来讲希望能帮你少走点弯路。我在做的这个跨平台网页应用目标用户既有用 Windows 的朋友也有在 Mac 和手机端 WebView 里操作的人。分片上传的兼容性问题不是 Chrome 一家说了算好在现代浏览器的 File API 和 Blob 切片都支持得不错让我能用一个统一的 JavaScript 方案去覆盖所有场景。整个方案的核心模块可以独立复用源码拿到就能改里面包含文件切片、并发控制、断点续传、进度回调以及后端接口约定这几块本篇文章会逐个拆开。1. 分片上传的方案选型与整体流程设计1.1 为什么必须拆分文件上传先说说直接将文件整体上传会遇到什么问题。普通multipart/form-data上传浏览器需要把整个文件读进内存再发出请求。我实测过 1GB 文件在千兆内网的传输情况并非网速不够而是网络连接只要稍微波动一下整个文件就得从头重传。如果是 4GB 的文件用户体验的崩溃程度不用我多讲任何一次断线都会让进度归零。分片上传的核心思路是把大文件切成固定大小的块每块作为一个独立请求去提交。这样做有三个直接好处单个分片的大小可控请求体不会太大后端处理起来不会因为大流量卡死内存。分片之间相互独立失败之后只重传失败的分片不需要整文件重来。后端接收分片后可以并发写入整体耗时比串行传输一个文件要短得多。从用户感知上分片上传还能配合进度条做成更细颗粒度的反馈不再是一个干等着的小转圈。我在做技术选型时也对比过 WebSocket 上传和 HTTP 分片上传。WebSocket 做二进制流传输在长连接场景下确实流畅但服务端实现复杂度高且多层代理对 WebSocket 的支持偶尔会有坑。相比起来HTTP 分片上传更通用跨平台和防火墙的兼容性更好所以我最终选择了 XMLHttpRequest 加 FormData 的组合。1.2 完整上传流程的阶段划分分片上传不是把文件切了就完事它需要一套完整的状态管理。整体上我把它分成五个阶段文件解析阶段读取文件元信息包括文件大小、文件类型、最后修改时间生成唯一指纹。切片计算阶段根据预设分片大小计算出分片总数和每片的起止字节位置。上传准备阶段调用后端接口询问哪些分片已经存在这一步用来支持断点续传和秒传。分片并发上传阶段按并发数控制请求数量上传过程中记录每个分片的状态。合并与校验阶段全部上传完成后通知后端合并分片后端做完整性校验并返回最终结果。这里有个细节经常被人忽略文件指纹的生成。如果直接拿文件名做唯一标识不同用户上传同名文件会冲突如果只拿文件大小和修改时间又可能出现重复文件被误判为已上传。我的做法是把文件大小、修改时间、文件名拼接后做一次 MD5这样既能支持同一文件秒传又不会误伤同名但内容不同的文件。后续文章会给出具体代码。在实际的项目里我还加入了一个“上传任务”对象来管理每个文件的完整状态包括当前进度、已上传分片集合、上传中分片集合和失败分片集合。这样无论是用户手动暂停还是网络异常都能在一次刷新后快速恢复而不是推倒重来。2. 前端核心实现从文件切片到并发上传2.1 文件读取与切片逻辑拆解前端拿到的文件对象是一个File对象它继承自Blob所以可以直接用Blob.prototype.slice()方法进行切片。这个方法非常关键它是分片上传的地基。不要自己去读FileReader把文件转成 base64 或 ArrayBuffer 再切那会直接把内存打爆。切片不是一个循环就完事了要注意边界条件。我写了一段可以直接用的核心切片代码function createFileChunks(file, chunkSize 5 * 1024 * 1024) { const chunks []; let start 0; while (start file.size) { const end Math.min(start chunkSize, file.size); const blob file.slice(start, end); chunks.push({ blob, start, end, size: end - start, index: chunks.length }); start end; } return chunks; }这段代码关键在于Math.min(start chunkSize, file.size)它能保证最后一片不会超出文件末尾。因为我一开始忽略这个边界导致最后一片的 size 为 0上传后跟后端合并数据时经常出现文件尾部缺字节的毛病。另一个容易掉坑的点是分片大小的选择。我测试过很多种参数实测下来 512KB 到 10MB 之间都有各自的适用场景。如果分片太小请求数量太多每个请求的 HTTP 开销会被放大整体吞吐量反而下降如果分片太大又回到整文件上传的老问题上网络波动导致重传成本变高。我最终在项目中默认选了 5MB局域网环境下可以调到 10MB弱网环境下调整为 1MB 更合适。切片之后每个分片本身就是一个Blob对象。这个对象可以直接丢进FormData但要记住一个坑FormData.append(file, blob, filename)的三个参数中第三个参数是文件名。如果不传部分后端框架解析 multipart 时拿不到文件名导致分片写入临时文件时没有后缀最后合并文件名会对不上。所以我在每个分片上传时都会显式传一个分片名例如filename.part.0。2.2 XMLHttpRequest 与 fetch 的上传实现对比分片上传的请求方式有两种主流选择XMLHttpRequest和fetch。如果你需要做上传进度条XMLHttpRequest是更顺手的方案因为它原生支持xhr.upload.onprogress事件。而fetch虽然 API 更新但对请求体的上传进度监听目前还需要借助流式传输某些浏览器兼容性没有 XHR 好。我的生产代码里用的是 XHR 封装的上传方法核心逻辑如下function uploadChunk({ url, chunk, index, total, identifier, filename }) { return new Promise((resolve, reject) { const formData new FormData(); formData.append(file, chunk.blob, ${identifier}_${index}.part); formData.append(index, index); formData.append(total, total); formData.append(identifier, identifier); formData.append(filename, filename); const xhr new XMLHttpRequest(); xhr.open(POST, url, true); xhr.timeout 30 * 60 * 1000; xhr.upload.onprogress (event) { if (event.lengthComputable) { // 单分片进度回调可以回传进度信息 } }; xhr.onload () { if (xhr.status 200 xhr.status 300) { resolve(xhr.responseText); } else { reject(new Error(Upload failed with status ${xhr.status})); } }; xhr.onerror () reject(new Error(Network error)); xhr.ontimeout () reject(new Error(Request timeout)); xhr.send(formData); }); }这里有几个细节是要特别注意的。第一xhr.open的第三个参数必须为true代表异步请求否则页面会卡死。第二timeout一定要设置因为大分片在弱网环境下可能长时间没有响应不设置超时会导致请求挂在那里不报错也不成功。第三xhr.onload只代表请求完成不代表上传成功所以必须校验 HTTP 状态码。使用fetch的写法其实也很简洁当你不需要细粒度进度条时可以用它async function uploadChunkWithFetch({ url, chunk, index, total, identifier }) { const formData new FormData(); formData.append(file, chunk.blob); formData.append(index, index); formData.append(total, total); formData.append(identifier, identifier); const response await fetch(url, { method: POST, body: formData }); if (!response.ok) { throw new Error(Upload failed with status ${response.status}); } return response.json(); }但是我必须说在实际生产环境中我大部分用的是 XHR因为上传进度条对用户来说是很重要的心理反馈。很多管理后台的用户看到进度条走到 99% 卡住会比看到 30% 卡住更焦虑一个准确、平滑的进度显示能显著提升用户对系统的信任感。2.3 并发控制与请求队列管理切好分片之后如果一口气把几十个请求同时发出去浏览器会直接罢工。不同浏览器的同域并发连接数是有限的Chrome 一般是 6 个左右超出部分会排队但排队过程不可控分片请求乱序到达后端容易让合并逻辑崩溃。所以前端必须手动做并发控制。我的做法是写一个简单的并发池用数组维护正在执行的任务超过设定的并发数就等待async function runWithConcurrency(tasks, limit) { const results []; const executing []; for (const task of tasks) { const promise task().then(result { results.push(result); return result; }); executing.push(promise); if (executing.length limit) { await Promise.race(executing).then(() { const index executing.indexOf(promise); executing.splice(index, 1); }); } } await Promise.all(executing); return results; }这个并发池的思路是把异步任务转换成 Promise维护一个当前在执行的 Promise 数组当数组长度达到上限时通过Promise.race等待最先完成的那一个然后把它从数组里移除腾出位置给新的任务。这样既保证了并发数上限又不会盲目等待第一个全部完成。实际使用中我还加入了“失败重试”逻辑。每个分片最多重试三次重试间隔采用指数退避策略分别是 1 秒、2 秒、4 秒。这个策略很重要。不加退避的话网络抖动恢复后所有失败分片会同时重试反而把带宽占满又造成新一轮失败。async function uploadWithRetry(uploadFn, retries 3) { try { return await uploadFn(); } catch (error) { if (retries 0) { throw error; } await sleep(1000 * (4 - retries 1)); return uploadWithRetry(uploadFn, retries - 1); } }这里我踩过最深的坑是盲目认为“重试”可以解决一切问题。有一次线上用户反馈上传一直失败我反复看日志发现是后端某个分片合并接口偶发 500 错误前端重试三次全都撞在同一个接口上。后来我把重试机制做了改进不是简单地重新请求而是先调用checkStatus接口确认后端状态如果后端已经不是接收状态就重新初始化上传流程问题才解决。总结下来重试并不是万能的重试之前一定要思考后端可能处于什么状态。3. 断点续传与文件指纹计算详解3.1 文件指纹的生成方案断点续传的核心在于如何判断“哪些分片已经上传过了”。这需要前后端对同一个文件达成相同的身份认证也就是生成文件唯一标识。我最早贪图省事直接用文件名作为 identifier。结果发现两个不同用户上传同名文件时后端会把他们当成同一个文件第二个用户上传时会命中“已存在分片”的判断导致秒传返回了一个错误的文件地址。这种问题在测试环境里不会暴露只有到真实项目里用户量上来后才明显。我现在的做法是读取文件的基础元数据拼接后做哈希async function generateFileIdentifier(file) { const metadata ${file.name}_${file.size}_${file.lastModified}; const encoder new TextEncoder(); const data encoder.encode(metadata); const hashBuffer await crypto.subtle.digest(SHA-256, data); const hashArray Array.from(new Uint8Array(hashBuffer)); return hashArray.map(byte byte.toString(16).padStart(2, 0)).join(); }这里用的是 Web Crypto API 的crypto.subtle.digest不需要额外引入第三方库浏览器原生支持且计算速度非常快。为什么不直接对整个文件做 MD5因为对大文件做完整哈希非常耗时而且占用大量内存用户上传前等几秒只为算个哈希体验太差。用文件元数据拼接哈希的方案几乎零成本虽然严格意义上存在极小概率的碰撞但在实际业务场景中足够使用。如果对安全性要求极高比如金融级别的系统可以采用抽样哈希的策略取文件前 1MB、中间 1MB、末尾 1MB 组合后做哈希以命中率和成本的平衡满足业务需求。3.2 秒传与断点续传的前后端配合逻辑分片上传的完整体验需要前后端紧密配合。前端在上传前先调用一个检查接口async function checkUploadStatus(identifier) { const response await fetch(/api/upload/status?identifier${identifier}); const data await response.json(); return data; }后端返回的 JSON 结构大概是这样的{ uploaded: [0, 1, 2, 5, 6], total: 10, status: uploading }uploaded数组表示已经上传成功的分片序号。前端拿到这个数组后就只上传缺失的分片。如果后端返回的uploaded数量等于total说明文件已经完全存在可以直接跳转到最终合并接口这就是秒传的实现原理。这里的合并接口也要谨慎设计。我的方案是后端在最后一个分片上传完成后自动触发合并流程同时提供一个手动触发的合并接口。前端在上传完所有分片后无论后端是否自动触发都会再调用一次合并接口。合并接口内部会做幂等处理即使重复调用也不会重复合并这样既保证了并发场景下的安全又简化了前端的调用逻辑。在做断点续传时还有一个很容易踩的坑前端上传过的分片在后端可能因为保存超时被清理了。比如用户退出了页面几个小时后重新上传同一个文件此时后端临时目录里的旧分片已经被人为清理。所以前端不能盲目相信后端返回的uploaded列表还要对比一下后端返回的文件大小和当前文件的分片数是否一致。如果不一致就要通知后端清理旧的临时数据重新开始。3.3 暂停、恢复与进度计算用户在上传大文件的过程中很可能会希望暂停。暂停不是说把请求取消就行而是需要处理好过程状态。我实现暂停功能的思路是在并发池的循环中加入一个状态变量class UploadManager { constructor() { this.isPaused false; this.uploadedChunks new Set(); } async pause() { this.isPaused true; } async resume() { this.isPaused false; await this.uploadRemainingChunks(); } }pause()方法内部只是把isPaused置为 true并发池中的新任务不会再启动但已经发出且正在传输中的请求无法被取消这是 JavaScript 异步模型的限制。用户看到的“暂停”其实是逐步停止的等待所有正在传输的分片完成后整个任务才真正停下来。在 UI 上需要给用户一个明确的反馈说明还有几个分片在收尾。进度计算也有一些学问。简单把“已上传的分片数除以总片数”作为整体进度很粗糙因为每个分片大小一致时还好但如果最后一片较小或者某些分片上传速度不同这个进度显示就会跳来跳去。我采用的是“按字节计算”的方式维护一个已上传字节数变量在每次分片上传成功后将size累加进去然后用已上传字节数 / 文件大小得到精确的进度。实测下来进度条平滑很多。4. 跨平台适配与后端接口约定的工程化落地4.1 跨浏览器与移动端 WebView 的兼容性处理我之所以老强调“跨平台”是因为这个网页应用最终会跑在 Windows 桌面端浏览器、macOS Safari、安卓原生 WebView、iOS 内嵌 WebView 等多端环境。别看这些都是浏览器行为差异非常明显。最大的坑出在 iOS Safari 和 WebView 对Blob.prototype.slice的参数要求上。老版本的 iOS Safari 要求slice的第二个参数必须显式传入且不传时返回的是空 Blob。我在做兼容处理时强制传入end并且用一个工具函数统一封装const safeSlice (blob, start, end) { if (blob.slice) { return blob.slice(start, end); } if (blob.webkitSlice) { return blob.webkitSlice(start, end); } if (blob.mozSlice) { return blob.mozSlice(start, end); } throw new Error(当前浏览器不支持文件切片功能); };第二个兼容性坑是FormData在部分低版本 WebView 中不支持直接追加 Blob 对象。我用了一种降级方案先将分片 Blob 转成 File 对象再追加const fileFromBlob new File([chunk.blob], ${identifier}_${index}.part, { type: application/octet-stream }); formData.append(file, fileFromBlob);这个做法在绝大多数现代 WebView 里都可以正常工作唯一的代价是需要额外创建一次 File 对象的引用不会复制文件内容所以内存占用很小。还有一个必须处理的点是超时设置的差异网络慢的时候 XHR 的timeout在部分浏览器中如果设得太大反而会失效。我的处理方式是给底层 XHR 对象设置一个合理的默认值同时采用应用层超时控制用setTimeout包一层超时后调用xhr.abort()。function uploadChunkWithTimeout({ ... }) { return new Promise((resolve, reject) { const xhr new XMLHttpRequest(); const timer setTimeout(() { xhr.abort(); reject(new Error(应用层超时请求已终止)); }, 10 * 60 * 1000); xhr.onloadend () clearTimeout(timer); // 其他逻辑... }); }这样处理之后即使某个平台的底层超时实现有 bug上层逻辑也能保证请求不会挂死。4.2 后端接口设计与合并策略前端写得再好后端不给力也是白搭。分片上传最考验后端的就是两个点一是分片存储的组织方式二是合并时的数据完整性。我的后端接口设计如下初始化接口/api/upload/init接收文件名、文件大小、分片大小返回 uploadId。分片上传接口/api/upload/part接收 uploadId、分片索引、分片内容。状态查询接口/api/upload/status接收 uploadId返回已上传的分片索引集合。合并接口/api/upload/merge接收 uploadId触发合并操作。合并操作的校验逻辑可以用一句话总结每个分片的后端接收大小必须与前端切片的期望大小一致。我在后端验证每个分片大小时会跟前端传过来的chunkSize做比对不一致就拒绝保存。因为有些代码库的slice实现会因为文件编码问题导致分片比预期大或小这种不一致如果提前不拦截合并后的文件会损坏。后端的合并我这里用一个 Node.js 的示例来说明const fs require(fs); const path require(path); async function mergeChunks(uploadId, totalChunks, destFilePath) { const chunksDir path.join(__dirname, uploads, uploadId); const writeStream fs.createWriteStream(destFilePath); for (let i 0; i totalChunks; i) { const chunkPath path.join(chunksDir, ${i}.part); const buffer await fs.promises.readFile(chunkPath); writeStream.write(buffer); } writeStream.end(); // 合并完成后清理临时目录 await fs.promises.rm(chunksDir, { recursive: true, force: true }); }这段代码是按顺序写入所有分片。如果要追求更高性能可以改成并发读取分片再按顺序写入但要注意避免文件句柄过多导致文件描述符耗尽。我测试过 100 个分片的场景串行读取和写入的速度完全可以接受瓶颈还是在网络上真到磁盘合并时反而是最不担心的一环。4.3 工程化配置与性能调优参数汇总分片上传的参数不能写死要做成可配置项最好在初始化时根据网络环境自动适配。我在项目中的默认配置如下配置项默认值适用场景分片大小5MB普通宽带环境通用性最好并发数3避免占用过多带宽平衡稳定性最大重试次数3应对网络抖动重试退避时间1s / 2s / 4s指数退避避免重试风暴超时时间10 分钟足够应对弱网环境下的单分片传输最大上传文件10GB超出时可提示客户端使用专用工具其中我要特别说明下并发数的选择。并发数过高会让浏览器和网络都不稳定。我曾经把并发数调到 8在千兆局域网环境下传输速度确实有提升但在普通宽带环境下大量并发请求同时挤占带宽每个请求的速度都变慢了反而整体吞吐量下降。最终在多数项目中维持在 3 到 5 之间既能利用多请求并发提升速度又不会因为抢占带宽而互相拖累。5. 常见问题排查与实操经验总结5.1 上线后的典型故障记录与排查思路分片上传上线后我收到过形形色色的 bug 反馈这里整理几个印象深刻的第一类是“上传完成后文件打不开”。排查下来大概率是切片时漏了最后一块或者后端合并时没有按照索引顺序写入。解决方案就是我在前面提到的Math.min(start chunkSize, file.size)边界处理以及后端mergeChunks里必须严格按照索引递增顺序处理。还有一个容易被忽略的细节就是分片的index字段在传递过程中被后端解析成字符串排序时按字典序而不是数字序导致index10排到了index2前面。后端排序时必须显式转换成整数。第二类是“上传过程中页面卡死”。这种情况在用户往分片上传表单中拖入超大文件时很容易触发。排查发现是FormData组装过程中如果直接 append 一个巨大的 Blob浏览器在主线程上执行了同步的内存分配导致页面短暂假死。解决方案是把文件切分和 FormData 组装放到Web Worker中执行主线程只负责接收进度事件。Web Worker 的使用很成熟唯一要注意的是 Worker 中无法访问FormData以外的一些 DOM API好在切分逻辑完全不依赖 DOM。第三类是“进度条卡在 99% 不动”。原因很凿凿前端计算进度时只统计已上传完成的字节数最后一个分片因为并发池正在等待别的分片完成还没真正开始上传所以进度一直显示 99%。等到最后一个分片开始并完成进度又直接跳到 100%中间缺少平滑过渡。我后来的解决方式是把“已经进入上传队列但尚未开始的分片”也计入总进度的分母并且给每个分片单独维护状态UI 层的进度条按状态加权计算这样 99% 的卡顿感就消失了。5.2 分片上传性能优化的实测对比性能调优这件事不能靠感觉。我在不同分片大小和并发数组合下做过一轮实测基准环境是一台普通 PC 加上千兆内网文件大小 2GB结果记录下来之后还挺有意思分片大小并发数总体耗时失败重试次数1MB3425s75MB3318s210MB3334s15MB5295s15MB8337s61MB5402s11可以看到并不是分片越小速度越快。分片过小带来了大量的 HTTP 请求和 multipart 解析开销。分片 5MB 加并发 5 在这个环境下表现最好。而分片过大会导致单个请求重试成本变高所以在弱网环境下应适当减小分片。还有一个优化策略是“自适应并发控制”让前端根据当前上传速度动态调整并发数。比如上传速度低于某个阈值时降低并发数减少因网络拥塞导致的超时重试速度快时增加并发数充分利用带宽。这个策略实现起来不复杂但我只把它放在了网络环境波动较大的移动端场景中PC 端固定并发数足够稳定。5.3 直接可复用的分片上传完整源码下面给出一份精简但完整可运行的分片上传核心类整合了前面所有的关键逻辑。你可以在项目中直接使用按实际接口路径修改即可。class ChunkedUploader { constructor(options) { this.file options.file; this.url options.url; this.chunkSize options.chunkSize || 5 * 1024 * 1024; this.concurrency options.concurrency || 3; this.retries options.retries || 3; this.onProgress options.onProgress || (() {}); this.uploadedBytes 0; this.chunks []; this.identifier ; } async init() { this.identifier await generateFileIdentifier(this.file); const status await checkUploadStatus(this.identifier); // status.uploaded 是一组已上传的分片索引 this.chunks createFileChunks(this.file, this.chunkSize); this.uploadedBytes this.chunks .filter((_, index) status.uploaded.includes(index)) .reduce((sum, chunk) sum chunk.size, 0); this.pendingChunks this.chunks.filter( (_, index) !status.uploaded.includes(index) ); } async start() { await this.init(); const tasks this.pendingChunks.map((chunk, offset) { return () this.uploadWithRetry(chunk); }); await runWithConcurrency(tasks, this.concurrency); await this.merge(); } async uploadWithRetry(chunk) { let lastError; for (let attempt 0; attempt this.retries; attempt) { try { await this.uploadChunk(chunk); this.uploadedBytes chunk.size; this.onProgress(this.uploadedBytes, this.file.size); return; } catch (error) { lastError error; await sleep(1000 * Math.pow(2, attempt)); } } throw lastError; } async uploadChunk(chunk) { const formData new FormData(); formData.append(file, chunk.blob, ${this.identifier}_${chunk.index}.part); formData.append(index, chunk.index); formData.append(total, this.chunks.length); formData.append(identifier, this.identifier); formData.append(filename, this.file.name); const response await fetch(this.url, { method: POST, body: formData }); if (!response.ok) { throw new Error(分片上传失败: ${response.status}); } } async merge() { const mergeUrl new URL(this.url /merge); mergeUrl.searchParams.set(identifier, this.identifier); mergeUrl.searchParams.set(filename, this.file.name); mergeUrl.searchParams.set(total, this.chunks.length); const response await fetch(mergeUrl.toString(), { method: POST }); if (!response.ok) { throw new Error(合并失败: ${response.status}); } return response.json(); } } // 工具函数 function createFileChunks(file, chunkSize) { const chunks []; let start 0; while (start file.size) { const end Math.min(start chunkSize, file.size); chunks.push({ blob: file.slice(start, end), start, end, size: end - start, index: chunks.length }); start end; } return chunks; } async function generateFileIdentifier(file) { const metadata ${file.name}_${file.size}_${file.lastModified}; const data new TextEncoder().encode(metadata); const hashBuffer await crypto.subtle.digest(SHA-256, data); return Array.from(new Uint8Array(hashBuffer)) .map(byte byte.toString(16).padStart(2, 0)) .join(); } async function runWithConcurrency(tasks, limit) { const executing []; for (const task of tasks) { const promise task(); executing.push(promise); if (executing.length limit) { await Promise.race(executing); const index executing.indexOf(promise); executing.splice(index, 1); } } await Promise.all(executing); } function sleep(ms) { return new Promise(resolve setTimeout(resolve, ms)); }这段源码看似不复杂但它把并发控制、重复上传的跳过、进度统计和最终合并全串起来了是我在实际项目中反复打磨后的简化版。如果你要用在多文件上传场景只需在外层包一个循环为每个文件创建一个ChunkedUploader实例即可。5.4 分片上传后续的扩展方向我在完成基础版本后又在真实线上项目中逐步加入了一些扩展能力这里分享几个我想推荐你后续可以做的方向。第一是“服务端临时文件自动清理”。分片上传如果中途放弃后端临时目录里会堆积大量半成品文件。我的做法是在后端加一个定时任务扫描超过 24 小时仍未合并的上传会话并清理。前端也要配合在检测到用户离开页面前调用一次取消接口通知后端清理。第二是“上传任务持久化”。利用浏览器 IndexedDB 保存当前上传状态包括 identifier、分片索引集合和文件元数据。这样用户误关页面后重新打开可以自动恢复列表并提示“继续上传”。这个功能看似简单但在企业内部系统中非常受认可。第三是“多文件队列与优先级”。管理后台经常需要批量上传一堆文件这时候可以做一个上传队列默认按文件大小排序小的先传大的后传。用户也可以手动调整优先级。实现上只需要给uploader实例加一个队列管理器控制同时启动的实例数量。我可以很坦诚地说分片上传这套方案并不新颖但它涉及的前后端配合细节非常多每一步都藏着小九九。有时候你觉得代码写对了跑一次才发现边界情况根本没覆盖。希望这篇实战分享能让你真正把这个功能在自己的应用中落地而不是看一眼原理就搁置了。如果你在实现过程中遇到问题不妨先把参数表里的分片大小和并发数调整一下试试很多看似复杂的问题往往就是这两个值没选对引起的。
阅读完成 · 觉得有帮助?