尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

大文件上传实战:Vue3+Axios分片上传与断点续传全解析

大文件上传实战:Vue3+Axios分片上传与断点续传全解析 做后台管理系统这几年大文件上传是绕不开的一道坎。以前我处理这种需求也简单粗暴input[typefile]选完文件直接axios.post一发把整个文件怼给后端。小文件倒还好一旦碰上几个 GB 的数据库备份、监控视频包或者设计源文件浏览器卡死、请求超时、后端 OOM 轮着来最后用户丢过来一句传个文件都传不上去你说气不气。后来我把这套逻辑从死磕整文件上传改成分片 并发 断点续传并且完整做了一遍前端源码层面的设计和实现才算是把这个功能彻底啃了下来。这期把示例代码和源码分析一起整理出来围绕 Vue 3 Axios 的分片上传方案讲清楚每一段代码为什么这么写、后端接口怎么配合、遇到卡顿和超时又该怎么排查。无论你是刚接触大文件上传还是已经做过一版但不满意这篇都适合静下心看完。1. 为什么大文件上传必须走分片这条路1.1 直接传整个文件的真实痛点先说说整文件上传在大文件场景下到底哪里不行。很多人以为无非是等久一点但实际远没有那么简单。第一是浏览器和 HTTP 层面的超时。一个大文件在网络传输中持续占用连接Nginx、后端网关或者云负载均衡器都有proxy_read_timeout、client_max_body_size这类限制。文件超过一定大小请求还没传完就被网关掐断前端收到的是一个 413 或者 504用户一脸懵文件其实连服务器都没上。这还只是表面问题后端的请求体接收也需要大量内存几个大文件同时上传服务端直接内存飙升严重的会把同一个进程上的其他接口一起拖死。第二是失败的恢复成本。整文件上传意味着全有或全无网络抖动一下断了进度归零从头再来。用户传了一个 2GB 的文件传到 80% 断了再传到 80% 又断了这种体验放在任何产品里都是劝退级别的。第三是进度条不真实。axios的onUploadProgress拿到的进度基于浏览器底层传输层整文件模式下这个进度确实也在更新但服务器接收进度和文件落盘进度是两回事而且断掉之后没有任何恢复手段。用户看到 99%等半天发现失败这个信任感打击比一开始就告诉他失败还要大。1.2 分片怎么解决了这些问题分片的核心思路非常朴素File对象继承自Blob而Blob天生支持按字节范围切块也就是slice方法。把一个大文件切成若干个小块每块单独发起一个上传请求。这个转变带来的直接好处有三个。第一单个请求体变小每个分片几 MB请求可以在几秒内完成远远低于网关超时阈值第二某个分片失败只需要重传那一片不需要整个文件重新上传断点续传成为可能第三多个分片可以并发上传配合浏览器同域并发限制合理调度整体吞吐量甚至比单请求传输更高。当然分片也带来了新的复杂度。分片之后后端需要知道这些分片属于哪个文件所有分片传完后要触发合并。前端需要计算文件唯一标识一般用 hash需要管理并发队列需要处理分片失败重试需要实时计算整体进度。这些正是本文后面源码部分要展开的内容。从工程角度看分片不是一种可选优化而是大文件上传的必选项。文件超过 100MB甚至是 500MB、1GB必须默认走分片方案。这也是整个示例设计的基本前提。2. 上传模块的整体架构与关键设计决策2.1 技术选型Vue 3 Axios SparkMD5示例代码基于 Vue 3 的组合式 API 编写我建议的配套方案是 Vite 构建、Element Plus 的进度条和上传组件做交互壳、Axios 发请求、SparkMD5 计算文件指纹。这套组合在当前生态里很成熟如果你用的是 Ant Design Vue 或者其他 UI 库替换成本也很低核心逻辑完全不受影响。选 Axios 而不是原生fetch一个很重要的原因是进度上报。fetch虽然也支持流式读取但要做上传进度需要自己封装ReadableStream的读取逻辑代码复杂度明显上升。Axios 在浏览器端底层走XMLHttpRequestonUploadProgress接口开箱即用。分片上传场景里每个请求的进度上报、取消、重试都很关键Axios 的拦截器和配置机制能省下不少事。选 SparkMD5 而不是用其他更快的 hash 算法是因为服务端校验文件唯一性的场景里 MD5 已经够用而且 SparkMD5 专门针对大文件做了增量读取设计逐片 append 进去内存占用可控。后面会看到它和分片方案配合得非常好。2.2 分片大小、并发数、hash 策略的参数设计这是所有面试里大文件上传的高频考点也是实际项目中最需要认真拍板的设计决策。我习惯用一个表格直接定参数后面所有代码都围绕这些参数展开。参数推荐值选择理由分片大小2MB - 10MB太小会导致请求数量暴增网络握手开销占比过高太大会让单片重试成本变高。我一般取 5MB并发数3 - 6浏览器同域 HTTP/1.1 连接上限是 6并发超过这个数请求会排队反而变慢hash 算法MD5 (SparkMD5)计算速度快冲突概率在文件唯一性校验场景下可接受重试次数3 次超过这个次数说明网络通环境有问题继续重试没有意义秒传判断上传前先查 hash服务端已存在同名同 hash 文件时直接返回成功省流量拿一个 1GB 的文件举例如果分片大小取 5MB一共 204 片。用 3 个并发去跑单片请求在普通带宽下大约 1-2 秒完成全部传完大约 2-3 分钟属于完全可以接受的范围。如果分片取 100KB1GB 文件要切 10000 多片光是建立连接的时间就够喝一壶的。这里还要重点解释一下 hash 为什么放在前端算。很多人会说MD5 后端算不就好了问题在于如果前端不上传任何标识后端就没法把分片归属到同一个文件上秒传和断点续传都无从谈起。前端的职责是先算出文件的唯一指纹带着这个指纹去问服务端这个文件你之前收过没再决定是跳过、续传还是全量上传。这是整套分片方案的入口。2.3 整体交互流程设计在贴代码之前先把完整流程在脑子里过一遍不然看代码容易断片。用户选择文件得到File对象。前端把文件按固定大小切块并用 SparkMD5 增量计算整个文件的 hash。拿着 filename 和 hash 请求后端的/upload/check接口。根据接口返回判断uploaded为空且exist为 false全新上传从分片 0 开始传。uploaded非空断点续传跳过已传分片。exist为 true服务端已经有同 hash 文件直接秒传完成。对需要上传的分片做并发调度每个分片独立请求/upload接口。所有分片传完后请求/upload/merge让服务端合并分片。合并完成整个上传闭环结束。这个流程里最容易被新手忽略的是先 check 再传这个思想。没有 check 步骤前端的 hash 白算断点续传也做不起来。后面代码里的checkFile函数就是干这件事的。3. 完整示例代码从文件选择到秒传断点续传3.1 文件分片与 hash 计算的工具函数第一个工具函数是分片。File继承自Blob所以可以直接调用slice。切完之后不要只保留Blob还要带上 index、size、当前分片在文件中的起始偏移量后面传给后端做校验和合并都很有用。const CHUNK_SIZE 5 * 1024 * 1024; // 5MB function createFileChunks(file, chunkSize CHUNK_SIZE) { const chunks []; let cur 0; let index 0; while (cur file.size) { const blob file.slice(cur, cur chunkSize); chunks.push({ chunk: blob, index, size: blob.size, start: cur, end: Math.min(cur chunkSize, file.size) }); cur chunkSize; index; } return chunks; }第二个工具函数是计算文件 hash。这里用 SparkMD5 的ArrayBuffer模式逐片FileReader.readAsArrayBuffer读取然后spark.append()进去。注意一次不能把整个大文件读进内存再算必须一片一片喂给 SparkMD5这也是它能处理大文件的原因。import SparkMD5 from spark-md5; function calcFileHash(file, chunks) { return new Promise((resolve, reject) { const spark new SparkMD5.ArrayBuffer(); const total chunks.length; let count 0; function loadNext(index) { if (index total) { const hash spark.end(); resolve(hash); return; } const reader new FileReader(); reader.onload (e) { spark.append(e.target.result); count; // 这里可以回调计算进度比如把 count / total 抛给 UI loadNext(index 1); }; reader.onerror () { reject(new Error(读取分片失败: ${index})); }; reader.readAsArrayBuffer(chunks[index].chunk); } loadNext(0); }); }这里有个容易踩的细节FileReader是有状态的onload和onerror每次读取前都要重新绑定到当前 reader 实例不能在外面只创建一次。如果复用同一个FileReader会遇到上一次读取还没结束就开始下一次读取的竞态问题。我在早期版本里就栽过这个跟头现象是 hash 算出来的值不稳定时对时错。3.2 断点续传与秒传的接口交互代码hash 算完之后下一件事是带着 hash 去问服务端。示例里假设后端提供了两个接口POST /upload/check和POST /upload语义分别是对账和传输分片。先看checkFile函数。它负责把这个文件是否已完整存在和这个文件已经传到第几片两个信息拿回来然后交给上传主流程做决策。import axios from axios; const http axios.create({ baseURL: /api, timeout: 30000 }); async function checkFile({ filename, hash }) { const { data } await http.post(/upload/check, { filename, hash }); // 约定返回结构 // { // exist: false, // uploaded: [0, 1, 2, 5], // 已经上传成功的分片 index // uploadId: xxx // 服务端为该文件生成的唯一上传会话 ID // } return data; }如果data.exist true前端可以直接提示文件已存在秒传成功如果data.uploaded不为空说明之前传过一部分只需要把未传的分片过滤出来继续传。这样断点续传就实现了。上传单个分片的函数也很简单。需要注意的是uploadId和当前分片的index属于元数据我用 FormData 字段传给后端而不是拼在 URL 上因为分片内容本身就是二进制流。async function uploadChunk({ uploadId, chunk, index, total }) { const formData new FormData(); formData.append(uploadId, uploadId); formData.append(index, index); formData.append(total, total); formData.append(file, chunk); const { data } await http.post(/upload, formData, { headers: { Content-Type: multipart/form-data }, // 单分片进度不需要精确到每秒接口成功后把整体进度往上推即可 onUploadProgress: (e) { // 可以在这里做单片进度但一般不用整体进度更好用 } }); return data; }3.3 完整的 Vue 3 上传组件主体逻辑把上面这些串起来就是一个可直接运行的上传组件。我简化了 UI 层样式保留关键流程。template div classuploader input typefile :disableduploading changehandleFileChange / div v-ifcurrentFile p文件名{{ currentFile.name }}大小{{ formatSize(currentFile.size) }}/p pmd5 计算进度{{ hashProgress }}%/p p上传进度{{ uploadProgress }}%/p el-progress :percentageuploadProgress / div v-if!uploading el-button typeprimary clickstartUpload开始上传/el-button el-button clickresetFile重新选择/el-button /div div v-else el-button typedanger clickhandlePause暂停上传/el-button /div /div /div /template script setup import { ref } from vue; import { ElMessage } from element-plus; import { createFileChunks, calcFileHash } from ./upload-utils; import { checkFile, uploadChunk, mergeFile } from ./upload-api; const currentFile ref(null); const fileChunks ref([]); const fileHash ref(); const uploading ref(false); const hashProgress ref(0); const uploadProgress ref(0); const uploadedIndexes ref(new Set()); const CHUNK_SIZE 5 * 1024 * 1024; let uploadId ; let cancelFlag false; function handleFileChange(e) { const file e.target.files[0]; if (!file) return; currentFile.value file; fileChunks.value createFileChunks(file, CHUNK_SIZE); uploadedIndexes.value new Set(); uploadProgress.value 0; hashProgress.value 0; } function formatSize(size) { if (size 1024) return size B; if (size 1024 * 1024) return (size / 1024).toFixed(2) KB; if (size 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) MB; return (size / 1024 / 1024 / 1024).toFixed(2) GB; } async function startUpload() { if (!currentFile.value || fileChunks.value.length 0) return; uploading.value true; cancelFlag false; try { // 第一步算 hash if (!fileHash.value) { fileHash.value await calcFileHash(currentFile.value, fileChunks.value); } // 第二步服务端对账 const checkResult await checkFile({ filename: currentFile.value.name, hash: fileHash.value }); if (checkResult.exist) { ElMessage.success(秒传成功服务器已有相同文件); uploadProgress.value 100; return; } uploadId checkResult.uploadId; const uploaded new Set(checkResult.uploaded || []); uploadedIndexes.value uploaded; // 第三步过滤出未传分片并发调度 const pendingTasks []; fileChunks.value.forEach((item, index) { if (!uploaded.has(index)) { pendingTasks.push({ index, ...item }); } }); // 第四步并发上传这里用 asyncPool 控制并发数 await asyncPool(3, pendingTasks, async (task) { if (cancelFlag) return; await uploadChunk({ uploadId, chunk: task.chunk, index: task.index, total: fileChunks.value.length }); uploadedIndexes.value.add(task.index); refreshProgress(); }); if (cancelFlag) { ElMessage.warning(上传已暂停); return; } // 第五步合并分片 await mergeFile({ uploadId, filename: currentFile.value.name, hash: fileHash.value }); ElMessage.success(上传成功); uploadProgress.value 100; } catch (err) { ElMessage.error(上传失败 (err?.message || 未知错误)); } finally { uploading.value false; } } function handlePause() { cancelFlag true; uploading.value false; } function refreshProgress() { const total fileChunks.value.length; const done uploadedIndexes.value.size; uploadProgress.value Math.floor((done / total) * 100); } /script这段代码走完一遍你就能理解整套分片上传的主干。剩余的关键是asyncPool并发控制器和mergeFile的细节我把它们放到源码剖析章节单独讲。4. 源码核心逻辑剖析并发控制、重试与恢复4.1 并发控制器的实现与原理asyncPool是整个上传模块里最有源码分析价值的一段。很多现成库可以直接引但自己写一遍能彻底搞懂并发调度的本质。async function asyncPool(poolLimit, tasks, iteratorFn) { const ret []; const executing new Set(); for (const task of tasks) { if (cancelFlag) break; const p Promise.resolve().then(() iteratorFn(task)); ret.push(p); executing.add(p); const clean () executing.delete(p); p.then(clean).catch(clean); if (executing.size poolLimit) { await Promise.race(executing); } } return Promise.all(ret); }思路用一句话概括每添加一个任务就await Promise.race(executing)等当前并发池里有任务完成后再继续添加下一个。executing用Set而不是数组是因为Set的删除操作是 O(1)而且可以避免数组splice带来的索引错乱问题。这里还需要说明一个很多人在并发控制上的误解并发数不等于越大越好。如果把并发拉满让所有分片同时上传浏览器在 HTTP/1.1 下对同一域名的连接数限制会形成排队而且无线程调度反而会让部分请求长时间饿死。所以示例里取 3是比较保守但稳定的值。如果你确定服务端部署在 HTTP/2 下可以适当放宽到 6-8。4.2 失败重试不能把请求失败全部甩给用户大文件上传最大的敌人是网络抖动。分片很小但任何一个分片失败都会导致最终合并失败所以必须有重试机制。重试放在uploadChunk内部做用递归或者 for 循环都行。我一般这样写async function uploadChunkWithRetry(params, retryCount 3) { for (let attempt 1; attempt retryCount; attempt) { try { return await uploadChunk(params); } catch (err) { if (attempt retryCount) { throw new Error(分片 ${params.index} 在 ${retryCount} 次重试后仍失败); } // 指数退避第一次失败等 500ms第二次等 1000ms第三次等 2000ms const delay Math.pow(2, attempt - 1) * 500; await new Promise((resolve) setTimeout(resolve, delay)); } } }指数退避的意义是避免重试风暴。如果所有失败分片同时在 200ms 后重试可能导致服务端瞬间被打满退避机制让每次重试的间隔逐步拉长给网络和服务端恢复的时间。4.3 断点续传的两种恢复方案与取舍断点续传有两个方向可以做第一种是纯前端本地恢复把已上传分片的 index 数组存到localStorage。优点是实现简单查询快不需要后端额外配合。缺点也很明显localStorage不可跨浏览器、不可跨设备用户清缓存就丢。而且本地记录可能被并发请求的状态干扰比如某个分片实际上传失败但本地已经标记成功。第二种就是示例用的方案上传前先请求/upload/check让服务端返回已上传分片列表。服务端是唯一可靠的数据源前端只管对账。这种方案多一次请求但换来的是绝对准确。实际项目中我推荐第二种尤其当文件很大、用户断点续传的诉求很强时服务端对账能避免假续传的坑。恢复时还有一个隐藏细节服务端在合并前必须校验分片的完整性不能因为前端说我传完了就直接合并。校验的手段通常是比对总大小、分片数量和每个分片的 hash。一旦校验不通过返回明确的错误码让前端补传缺失分片。这个对账逻辑前后端要一起定不是前端单方面的事。5. 真实项目里最容易踩的坑及避坑方案5.1 文件 hash 计算卡死主线程大文件 hash 计算的耗时可能远超你想象。我曾经拿一个 2GB 的视频文件跑 MD5主线程直接卡了 30 多秒用户看着页面假死反复点按钮体验极其糟糕。原因在于FileReader.readAsArrayBuffer虽然本身是异步的但 SparkMD5 的append操作和文件读取调度都在主线程执行读取一个 5MB 分片、做一次增量 hash 运算这部分计算在 JS 单线程上是有真实开销的。文件越大累计耗时越长。缓解的办法有两层。第一层是给用户明确的 hash 计算进度反馈至少让他知道程序在工作第二层是最终方案把 hash 计算放到 Web Worker 里这部分我会在下一章单独展开。如果你在第一版方案里不想引入 Worker至少要在 UI 上卡一个 loading 遮罩防止用户重复操作。5.2 axios 的上传进度数据单位与精度陷阱Axios 的onUploadProgress回调拿到的event.loaded和event.total单位是字节这个没问题。但如果你直接用分片的进度去算整体进度会发生进度条跳变因为每个分片大小相同计算方式应该是(已成功分片数 / 总分片数) * 100而不是把所有分片的loaded加在一起除以文件总大小。后者在分片并发上传时多个事件并行触发loaded 是累计还是当前请求的很容易搞混。我的建议是不要过度依赖单片的onUploadProgress而是用分片状态机来驱动整体进度每个分片只有pending / uploading / success / failed四种状态整体进度 成功分片数 / 总分片数。这样进度条是单调递增的不会出现跳变或者回退。5.3 并发上传时浏览器连接数打满HTTP/1.1 下浏览器对同一域名的并发连接数默认是 6。如果你把分片并发数也设为 6再加上页面本身还有其他 ajax 请求就会有一部分请求进入排队。排队不是最致命的更麻烦的是如果一个请求卡住不返回会一直占着连接池名额导致其他请求饥饿。避坑方案有两个方向。一是把分片并发数控制在 3-4给其他请求留出余量二是把所有上传请求的timeout设置合理不要让一个分片无限等待。示例代码里axios.create的timeout: 30000就起到了这个兜底作用。5.4 服务端合并超时与 Nginx 限制即使前端分片全部传完最后一步合并请求也可能失败。合并通常在后端读取所有分片文件写入一个大文件文件越大合并越耗时一旦超过 Nginx 的proxy_read_timeout前端拿到 504用户会以为上传失败但分片实际已经全到服务器了。这类问题不能只在前端兜底需要和后端协作。常用办法是合并接口改为异步任务后端先返回merge_id前端轮询合并状态或者在后端合并逻辑里做流式写入避免一次性把大文件读入内存。另外 Nginx 的client_max_body_size需要调大尤其是合并接口如果仍以multipart形式接收元数据限制同样要放宽。前后端联调时最好先约定这些参数值而不是等生产环境炸了再排查。5.5 暂停功能不是取消请求那么简单很多人在实现暂停时直接调用AbortController.abort()把进行中的请求全部取消。这样做确实可以停止上传但下次恢复时正在传输但没传完的分片会被服务端识别为未上传分片需要重新传。如果分片比较多取消重传的成本就很大。更合理的暂停方案是一旦暂停把并发池里还没开始的请求拦截住标记cancelFlag true让已经发出的请求继续跑完但不再启动新的请求。等用户点继续上传时重新走/upload/check对账此时已完成的分片会被服务端标记前端自动跳过。这样暂停是平滑的不会造成额外的分片重传。这才是真正有效的断点续传暂停。6. 进阶方向用 Web Worker 把 hash 计算赶出主线程6.1 为什么要用 Worker 以及基本原理前面提到 hash 计算会阻塞主线程本质原因是大量文件读取和 MD5 分片累加计算发生在 UI 线程上。浏览器的主线程要负责渲染、事件响应、JS 执行被一个 2GB 文件的 hash 计算占满时整个页面就冻住了。Web Worker 提供了一个独立的 JS 执行线程。Worker 里没有 DOM 访问权限不能操作 UI但可以执行纯计算逻辑包括ArrayBuffer的读取和哈希运算。好消息是File对象本身可以被结构化克隆到 Worker 中你在主线程已经拿到的file对象可以直接作为参数传给 Worker不需要重新从磁盘读文件。Blob 切片也能在 Worker 内部进行所以更优雅的做法是连分片逻辑也一起丢进去。用 Worker 重构后的流程变成主线程把file和chunkSize传给 WorkerWorker 内部完成 slice、读取、SparkMD5 累加最后把 hash 和分片总数通过postMessage传回主线程。整个过程中主线程完全空闲用户还能正常操作页面。6.2 Worker 代码骨架与 Vite 注意事项在 Vite 项目里创建 Worker可以用new Worker(new URL(./hash.worker.js, import.meta.url), { type: module })这样 Worker 内部可以直接import其他模块。先看hash.worker.js的代码这里使用importScripts引入 SparkMD5 的浏览器版本或者直接用 ESM 导入// hash.worker.js import SparkMD5 from spark-md5; self.onmessage async (e) { const { file, chunkSize } e.data; const spark new SparkMD5.ArrayBuffer(); const totalChunks Math.ceil(file.size / chunkSize); let loadedChunks 0; for (let index 0; index totalChunks; index) { const start index * chunkSize; const end Math.min(start chunkSize, file.size); const blob file.slice(start, end); const arrayBuffer await blob.arrayBuffer(); spark.append(arrayBuffer); loadedChunks; // 向主线程报告 hash 计算进度 self.postMessage({ type: progress, progress: Math.floor((loadedChunks / totalChunks) * 100) }); } self.postMessage({ type: done, hash: spark.end(), totalChunks }); };主线程侧的调用变成这样function calcFileHashInWorker(file, chunkSize) { return new Promise((resolve, reject) { const worker new Worker(new URL(./hash.worker.js, import.meta.url), { type: module }); worker.onmessage (e) { const { type, progress, hash, totalChunks } e.data; if (type progress) { hashProgress.value progress; } else if (type done) { worker.terminate(); resolve({ hash, totalChunks }); } }; worker.onerror (err) { worker.terminate(); reject(err); }; worker.postMessage({ file, chunkSize }); }); }有几个实际操作层面的注意事项第一Worker 里blob.arrayBuffer()的兼容性很好现代浏览器都支持。如果不放心可以用FileReader的 Worker 版本但代码会多一点。第二worker.terminate()必须在拿到结果后调用否则 Worker 线程一直驻留浪费内存。第三Vite 打包时如果用new Worker(new URL(...))这种写法Vite 会自动处理 Worker 的打包和资源路径不需要额外配置。如果你是 webpack 项目可能需要worker-loader或者配置worker规则这点换构建工具时要特别注意。第四Worker 里不能直接操作 DOM所以 hash 进度只能通过postMessage传回主线程由主线程更新页面进度条。这个和主线程内的calcFileHash回调进度在体验上是等价的但主线程不会再卡顿。6.3 分片上传进一步优化的可行方向Worker 解决了 hash 计算的卡顿问题之后大文件上传这套方案就基本完备了。如果还想继续优化有几个方向可以参考文件类型差异化策略视频、压缩包这类文件上传失败率较高可以为它们设置更小的分片和更高的重试次数普通文档则保持默认参数。服务端分片缓存清理如果用户上传中断后再也没回来服务端存了一堆孤儿分片需要定时任务清理否则磁盘会被拖垮。上传完成后的本地记录清理秒传和断点续传的记录会一直留在服务端需要设计存储过期策略。针对不同网络环境的自适应策略通过实时监测上传速度的动态调整并发数和分片大小。这个可以做得非常深但一般中小项目够用就行不必过度设计。我在实际项目里最常被问到的一个问题是有了 Worker 和分片是不是所有上传场景都应该用这套方案不是。小文件走这套流程反而会因为 hash 计算和对账请求增加额外耗时一个 50KB 的图片没必要算 MD5、查接口、分片并发。最稳妥的做法是先设置一个阈值比如 50MB 以下走普通直传超过阈值才走大文件分片链路两种策略共存按文件大小自动路由。大文件上传这个需求看上去简单真正做扎实却要从分片策略、并发调度、服务端对账、断点恢复、性能优化好几个层面逐一打磨。我自己的体会是这套方案的难点不在于某一个 api 的用法而在于把整条链路的边界条件和失败处理都想清楚。如果你正在做类似功能建议照着上面的思路先跑通主流程再把 hash 卡顿、断点恢复这些优化项逐个加上去不要一上来就追求一步到位。
返回列表