资讯详情

大文件分片上传实战:断点续传与前端加密方案详解

📅 2026/10/21 5:09:14 | 华诺云谱 👁 阅读
大文件分片上传实战:断点续传与前端加密方案详解
做上传功能最烦的不是写不出代码而是文件一大各种隐藏问题全冒出来了。2GB 的压缩包传到一半断了用户那边直接喊崩溃传给某个存储服务网关超时卡死换台手机拍照传原图内存直接顶满。我去年处理过一个模拟项目客户要传单文件好几个 GB 的影像数据前端不能装任何插件还要断点续传、要加密后端最好能证明数据没被改过。折腾一圈后我把完整方案沉淀了下来核心就一句话大文件上传必须分片分片之后才有资格谈加密和续传。这篇文章不打算写什么理论纲领直接把我用的源码结构、加密选型、续传思路和踩坑记录都摆出来。适合正在做文件上传相关功能的前端同学也适合被后端拉着联调“分片接口”的运维或全栈朋友。你看完能直接落地甚至可以直接复制里面的核心代码去改。1. 大文件分片上传这件事先看清全貌1.1 分片解决的三个实际问题很多人以为分片只是为了“断点续传”这其实是个误区。分片解决的是三个完全独立的问题断点续传只是分片之后顺手带来的一个红利。第一个问题是浏览器的请求体积限制。虽然 HTTP 协议本身不限制 body 大小但网关、负载均衡、Web 容器都有默认限制。我见过某个环境里上传超过 200MB 就返回 413调大以后又触发服务端内存溢出的因为服务端代码用了readFile把整个文件读进去了。分片以后单片 5MB无论网关还是服务端都能轻松应付。第二个问题是失败重试的成本。一个 1GB 文件整体上传失败重试就得从头再来。但拆成 200 片之后哪一片失败就传哪一片其他 199 片不用动。这个道理很简单但在实际项目里很多人优先把接口调通却忘了在接口外面做“重试单元”的划分。第三个问题最容易忽略是并行度。单文件上传无论怎么优化都只有一条连接在跑遇到网络抖动时吞吐量上不去。切分以后可以控制并发数比如同时传三片相当于把一条窄路变成三条并行通道尤其在高带宽、低延迟的局域网场景下速度提升非常明显。但注意并发数不是越大越好浏览器对同域名的连接数有限制HTTP/1.1 下一般是 6 个超过以后排队反而更慢。分片的大小我一般选 5MB 到 10MB 之间。太小的片会让请求数爆炸比如 1GB 文件切成 1MB那就是 1024 个请求服务端压力大前端遍历也有额外开销。太大的片又失去了分片的意义续传粒度太粗单片传输时间过长失败后重试成本高。最终我在这套方案里用了 5MB兼容性和性能比较均衡。1.2 一套完整的流程长什么样在写代码之前先要把整个流程走一遍脑中有图代码才不会乱。我习惯把上传拆成四个阶段准备、传输、校验、合并。准备阶段做的事情是计算文件唯一标识也就是文件指纹。这一步非常关键因为它同时服务于秒传和断点续传。文件指纹算完以后前端会调用后端的“查询接口”把指纹传过去后端根据指纹返回文件是否已存在、已上传了哪些分片。如果文件已存在直接提示“秒传成功”整个上传流程结束。如果不存在前端就开始按序切分文件每一片都独立编号从 0 开始。切分不是一次性把所有分片读进内存而是借助 File 对象的 Blob 切片能力按偏移量动态读取。也就是说切片是“逻辑切片”并不是物理上把大文件拆成了多个临时文件。传输阶段就是逐个上传分片。每个分片请求携带的信息至少包括文件指纹、分片总数、当前分片序号、分片原始大小。如果有加密还要带上加密后的实际大小和加密参数。传输过程中前端会在本地记录哪些分片已经成功以便中断后恢复。校验阶段分两层。第一层是传输完整性每个分片上传时后端可以校验分片的哈希值防止数据在传输过程中损坏。第二层是文件完整性所有分片传完后后端合并成一个完整文件再对合并后的整体文件计算一次哈希和前端的文件指纹比对。如果不一致说明分片组合逻辑或加密处理有问题。合并阶段就是把临时存放的分片按序号拼接成完整文件。合并可以由后端主动触发也可以等前端调用“合并接口”来触发。推荐后者因为合并是重操作如果前面还有分片没传完就触发合并只会得到错误结果。合并完成后后端清理临时分片返回最终文件的访问路径。这套流程看着简单实际落地时加密加进来流程就会多几个岔路口。第 3 节我会专门说加密选型这里先把指纹和续传的细节讲透。2. 断点续传的核心逻辑指纹、状态与接口2.1 文件指纹秒传与续传的开关断点续传的实现方式可以有很多种最简单粗暴的是前端本地保存一个已上传列表刷新页面后检查这个列表跳过已经成功的分片。这种办法只适合自己玩的小项目换个浏览器、清一下缓存续传能力就没了。真正的续传必须让后端参与后端才是分片状态的最终权威。要让后端识别“同一个文件”就必须给文件一个唯一标识。我常用的方式是计算整个文件的 MD5 哈希或者更稳妥一点用 SHA-256。但是这里有一个现实问题大文件的整体哈希计算非常耗时。一个 2GB 的文件即使用 SparkMD5 这种流式算法在普通机器上跑完也要好几秒甚至超过十秒。用户上传时看着“计算哈希中”的进度条体验很差。所以实际项目里我更喜欢用抽样哈希也叫快速指纹。取文件头部 2MB、中间 2MB、尾部 2MB再加上文件大小拼成一段数据后计算 MD5。抽样哈希的碰撞概率比完整哈希高但对大多数业务场景完全够用。你可以把“完整哈希”和“抽样哈希”理解为两种不同精度的“身份证”完整哈希是 DNA 鉴定抽样哈希是看脸部特征。判断同一个人脸部特征大概率就够了没必要每次都抽血。但要记住一个前提如果业务对文件一致性有法律级别或审计级别的要求比如医疗影像、合同文件那抽样指纹不够必须在最后合并完成后做一次整体哈希校验。也就是不要把“快速指纹”当成“完整性校验”这是两个维度的东西。另外算指纹要放在加密之前还是之后我强烈建议对原始文件算指纹。因为加密之后的数据如果作为指纹基准同一个原始文件每次用不同密钥加密指纹就不同了秒传和续传都会失效。原始文件算出来的指纹稳定、可复现适合做去重和续传依据。加密数据的完整性校验单独用分片哈希去解决。2.2 状态记录与接口约定断点续传需要回答一个问题已经传了哪些分片前端知道不够后端必须记住。后端的实现方式有几种用数据库表记录用 Redis 记录分片信息或者直接在磁盘上按“文件指纹/分片序号”命名临时文件。传完一片磁盘上就存在一个文件扫描目录就能知道进度。我常用的接口是三个第一个是查询接口前端传入文件指纹后端返回一个对象里面包含三个字段exist表示文件是否已经完整上传chunks表示已收到的分片序号数组size表示文件总大小。如果exist为 true前端直接走秒传逻辑如果chunks里有部分数据前端只上传缺失的片。第二个是分片上传接口前端上传一个分片同时提交文件指纹、分片序号、分片总数等元数据。后端先检查这个文件指纹对应的临时目录是否存在不存在就创建然后接收分片数据写成临时文件。这个接口要注意分片序号必须由前端显式传入不能依赖文件名排序否则遇到超过 10 片时像 1、10、11、2 这样的顺序会把合并逻辑坑惨。第三个是合并接口前端在所有分片上传完成后调用后端把临时目录里的分片按序号排序、逐个拼接生成最终文件。合并完成后再看临时目录是否要清理。这里有个容易踩的坑分片的默认序号是从 0 开始还是从 1 开始我的建议是固定的从 0 开始跟数组下标对齐前后端约定永远不改变。这样前端在判断“缺失哪些分片”时可以直接用数组索引后端排序时也只用数字比较不会混入字符串排序问题。状态记录还有一个细节后端在接收分片时不能只依赖前端传的“文件大小”来校验万一前端数据被截断后端会拿到不完整的片。比较稳妥的做法是前端每个分片附带一个该分片的原始哈希后端校验一致后再落盘。如果校验失败直接返回错误前端重传这一片。这样服务端就不会存下一堆损坏的临时文件合并阶段也能少很多莫名其妙的 bug。3. 加密到底加在哪一层先想清楚再动手3.1 三个加密场景别混为一谈加密这个词在文件上传里特别容易混淆。我在和别人讨论方案时经常发现双方说的“加密”根本不是一回事。至少有三个完全不同的场景第一个是传输加密也就是 HTTPS。这是所有方案的地基。只要数据还在网络上传输就必须有 TLS 加密否则前面做的所有工作在抓包工具面前都是透明的。实际上如果只是防止数据在网络上被窃听把站点切到 HTTPS 就已经解决了大部分问题根本不需要额外在前端做加密。第二个是端到端加密也就是数据从浏览器端开始就是密文服务端、存储端都接触不到明文。这种场景常见于网盘中的私密文件、企业内部的合规数据。前端用密钥加密分片密钥本身不经过服务器或者经过服务器但以密文形式保存。端到端加密的代价是服务端无法对数据内容做病毒扫描、图片缩略图等处理因为看到的全是乱码。第三个是存储加密也就是数据落盘以后是密文。这个一般在服务端完成只要对象存储服务开了 SSE 加密或者数据库层面做了透明加密就能满足很多合规需求。存储加密对前端完全透明业务代码无感。我在这套方案里默认采用“传输加密 前端分片加密”的组合主要解决的是两个核心诉求一是防止网络层泄露二是让服务端拿到的分片即使被拖走也无法解析出原始内容。但必须说清楚这种不算严格的端到端加密因为密钥最后还是会被服务端获取服务端如果被完全攻破密文还是能被解开。真要端到端密钥管理要单独设计。3.2 分片加密的正确姿势每片独立IV确定了前端要加密接下来要考虑用什么算法、怎么处理密钥、IV 怎么管理。算法选择上我用的是 AES-GCM。AES 是国际通用的对称加密算法GCM 模式自带认证标签也就是说加密出来的数据不仅能保密还能校验有没有被篡改。AES-GCM 已经被各大浏览器的 Web Crypto API 原生支持调用crypto.subtle.encrypt就能完成不需要引入额外依赖。密钥管理是重点。严格来说前端保存的密钥也不能硬编码在 JS 里因为 JS 代码对用户是可见的硬编码等于没有密钥。我常用的做法是让后端提供一个临时密钥下发接口前端在开始上传前调用接口获取一次性密钥。这个密钥绑定当前上传任务任务结束后失效。这个方案仍然是服务端可控的不是端到端但实现简单、管理方便。IV 的处理是很多人忽略的地方。AES-GCM 的 IV 不需要保密但必须保证每个分片使用不同的 IV而且加密时用了哪个 IV解密时也必须用同一个。我会为每个分片生成一个随机的 12 字节 IV然后把 IV 连同密文一起上传给后端后端合并时从分片元数据里取回 IV 再逐个解密。这里有个非常关键的细节分片序号必须参与加密认证。AES-GCM 额外支持附加认证数据 AAD可以把分片序号、文件指纹这些信息加进去。这样即使攻击者把分片改成了另一个文件的合法分片合并时也会因为 AAD 不匹配而校验失败。简单理解IV 管“这一片用了什么随机数”AAD 管“这一片必须属于谁”。加密后的分片大小会变化。AES-GCM 密文比明文多 16 字节的认证标签这意味着不能直接拿分片原始大小做校验。后端的临时分片长度是加密后的长度合并时先解密再按解密后的明文做拼接。3.3 加密和断点续传怎么配合加密不是额外塞进去的一个步骤它必须跟分片上传流程融合。我试过两种方案第一种是“先进内存读整块文件整体加密再切分”第二种是“先分片再对每个分片单独加密”。实际操作后我坚定选择第二种。第一种方案的问题在于内存。一个 200MB 的文件整体加密后生成的密文还在内存里切分也要在大数组上操作移动端浏览器很容易崩溃。而且整体加密会产生一个“加密后的完整文件”这个文件没有原始文件的指纹特征续传判断非常麻烦。第二种方案每片独立加密天然适配分片上传流程。每个分片的切片操作、指纹操作、加密操作、上传操作可以流水线处理内存占用低某一个分片加密失败也不影响其他分片。加密后的每个分片自带 IV 和认证标签后端可以边收边校验。加密对续传恢复的影响也很直观。在断点恢复时前端拿到后端返回的已上传分片列表剩下的工作就是重新从原文件切片、给缺失的分片重新生成 IV、重新加密、重新上传。因为每个分片独立加密完全不需要关心之前加密过的分片是什么内容。提示密钥不能在续传时更换。如果上传进行到一半用户刷新了页面密钥过期了那前端拿到新密钥后已上传的分片用的是旧密钥合并时后端就无法解密。最简单的处理是密钥有效期设得足够长比如 24 小时或者在断点恢复时后端把“当前任务正在使用的密钥 ID”连同已上传分片列表一起返回。4. 源码实现分片上传、加密、断点续传怎么捏合到一起4.1 前端核心代码切片、指纹、加密、上传、恢复下面这份代码是我在模拟项目里用过的精简版核心逻辑完整你可以直接抄去改。环境是浏览器使用 Web Crypto API 做 AES-GCM 加密上传用 XMLHttpRequest这样能拿上传进度。const CHUNK_SIZE 5 * 1024 * 1024; // 5MB const CONCURRENCY 3; function createChunks(file) { const count Math.ceil(file.size / CHUNK_SIZE); const chunks []; for (let i 0; i count; i) { const start i * CHUNK_SIZE; const end Math.min(start CHUNK_SIZE, file.size); chunks.push({ index: i, blob: file.slice(start, end), size: end - start, }); } return chunks; } async function computeFastHash(file) { // 抽样哈希取头、中、尾各2MB加文件大小 const size file.size; const head file.slice(0, 2 * 1024 * 1024); const middle file.slice( Math.floor(size / 2) - 1024 * 1024, Math.floor(size / 2) 1024 * 1024 ); const tail file.slice(size - 2 * 1024 * 1024, size); const buffer await new Response( new Blob([head, middle, tail, String(size)]) ).arrayBuffer(); const digest await crypto.subtle.digest(SHA-256, buffer); return Array.from(new Uint8Array(digest)) .map((b) b.toString(16).padStart(2, 0)) .join(); } function generateIV() { return crypto.getRandomValues(new Uint8Array(12)); } async function getUploadKey(fileFingerprint) { // 从后端换取本次上传的一次性密钥 const resp await fetch(/api/upload/key, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fingerprint: fileFingerprint }), }); const data await resp.json(); return base64ToBytes(data.key); } async function encryptChunk(chunkBlob, key, aad) { const iv generateIV(); const chunkData await chunkBlob.arrayBuffer(); const cryptoKey await crypto.subtle.importKey( raw, key, { name: AES-GCM }, false, [encrypt] ); const encrypted await crypto.subtle.encrypt( { name: AES-GCM, iv, additionalData: aad }, cryptoKey, chunkData ); return { iv, encrypted }; } class ChunkUploader { constructor(file, fingerprint) { this.file file; this.fingerprint fingerprint; this.chunks createChunks(file); this.uploadedIndexes new Set(); this.activeCount 0; this.queue []; this.totalChunks this.chunks.length; } async prepare() { // 查询后端哪些分片已上传 const resp await fetch(/api/upload/check, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fingerprint: this.fingerprint, size: this.file.size }), }); const data await resp.json(); if (data.exist) { return { exist: true, status: done }; } this.uploadedIndexes new Set(data.chunks); this.key await getUploadKey(this.fingerprint); return { exist: false, status: resume }; } uploadSingle(index) { return new Promise(async (resolve, reject) { const chunk this.chunks[index]; const aad new TextEncoder().encode(${this.fingerprint}:${index}); const { iv, encrypted } await encryptChunk(chunk.blob, this.key, aad); const formData new FormData(); formData.append(fingerprint, this.fingerprint); formData.append(index, index); formData.append(total, this.totalChunks); formData.append(size, chunk.size); formData.append(file, new Blob([encrypted]), chunk-${index}); const xhr new XMLHttpRequest(); xhr.open(POST, /api/upload/chunk); xhr.upload.onprogress (e) { if (e.lengthComputable) { // 这里可以回调进度 } }; xhr.onload () { if (xhr.status 200 xhr.status 300) { this.uploadedIndexes.add(index); resolve(); } else { reject(new Error(xhr.responseText)); } }; xhr.onerror () reject(new Error(network error)); xhr.send(formData); }); } async uploadAll(onProgress) { const needUpload this.chunks.filter( (c) !this.uploadedIndexes.has(c.index) ); let finished this.chunks.length - needUpload.length; for (let i 0; i Math.min(CONCURRENCY, needUpload.length); i) { this.runWorker(onProgress); } return new Promise((resolve, reject) { this.resolveFn resolve; this.rejectFn reject; this.onProgress onProgress; this.needUpload needUpload; this.finishedCount finished; this.injectWorker(); }); } }这段代码的思路是先用createChunks逻辑切分computeFastHash算指纹prepare阶段连完成后端查询和密钥换取encryptChunk做 AES-GCM 加密最后用 XMLHttpRequest 上传。并发控制我用了一个简单的 worker 机制限制同时只有 3 个分片在传。完整的并发队列代码我在这里补一个更清晰的小片段避免你直接抄上面的半成品踩坑class UploadQueue { constructor(uploader, concurrency 3) { this.uploader uploader; this.concurrency concurrency; this.running 0; this.pending []; this.done 0; this.success []; this.failed []; } push(task) { this.pending.push(task); this.next(); } next() { while (this.running this.concurrency this.pending.length 0) { const task this.pending.shift(); this.running; task() .then((res) { this.success.push(res); }) .catch((err) { this.failed.push(err); }) .finally(() { this.running--; this.done; this.next(); }); } } }实际使用时把所有缺失分片的上传任务塞进UploadQueue队列内部自动维护并发数。等队列跑完检查failed数组把失败的分片重试最多重试两次。重试仍失败的记录断点位置提示用户刷新后再次恢复。4.2 后端核心代码接收、校验、合并后端我用 Node.js 写框架假设是 Express目录结构很简单临时分片放/tmp/upload/${fingerprint}合并后的最终文件放/data/files/。你可以换成任意语言逻辑是通用的。先看分片上传接口const fs require(fs); const path require(path); const crypto require(crypto); const UPLOAD_TMP /tmp/upload; const FINAL_DIR /data/files; app.post(/api/upload/chunk, async (req, res) { // 假设 multer 已经处理了 multipart/form-data const { fingerprint, index, total, size } req.body; const file req.file; if (!file) { return res.status(400).json({ error: no file part }); } const chunkDir path.join(UPLOAD_TMP, fingerprint); fs.mkdirSync(chunkDir, { recursive: true }); const chunkIndex parseInt(index, 10); const chunkPath path.join(chunkDir, String(chunkIndex)); const buffer fs.readFileSync(file.path); const receivedHash crypto.createHash(sha256).update(buffer).digest(hex); // 这里可以校验分片的认证标签解密后再算原始分片哈希 // 简化起见先记录加密分片哈希 fs.writeFileSync(chunkPath, buffer); fs.unlinkSync(file.path); res.json({ received: true, index: chunkIndex }); });这个接口的重点是分片以“文件指纹/分片序号”落在临时目录序号就是文件名。虽然逻辑简单但并发上传时存在一个隐患同指纹目录下多个分片同时写入文件系统层面不会有问题因为文件名不同。再看查询接口返回已存在的分片序号列表app.post(/api/upload/check, (req, res) { const { fingerprint, size } req.body; const chunkDir path.join(UPLOAD_TMP, fingerprint); let chunks []; if (fs.existsSync(chunkDir)) { chunks fs.readdirSync(chunkDir) .map((name) parseInt(name, 10)) .filter((n) !isNaN(n)) .sort((a, b) a - b); } const filePath path.join(FINAL_DIR, ${fingerprint}.bin); const exist fs.existsSync(filePath) fs.statSync(filePath).size Number(size); res.json({ exist, chunks }); });合并接口是最后一步必须等所有分片都到达才能执行。合并时除了按序号拼接文件还要做两步校验一是分片数量是否完整二是合并后的总长度是否等于前端声明的原始文件大小。app.post(/api/upload/merge, async (req, res) { const { fingerprint, total, size, keyId } req.body; const chunkDir path.join(UPLOAD_TMP, fingerprint); if (!fs.existsSync(chunkDir)) { return res.status(404).json({ error: no chunks found }); } const chunkNames fs.readdirSync(chunkDir); if (chunkNames.length ! Number(total)) { return res.status(400).json({ error: chunk count mismatch }); } // 从 keyId 对应的地方拿到密钥材料 const key getKeyById(keyId); const finalPath path.join(FINAL_DIR, ${fingerprint}.bin); const writeStream fs.createWriteStream(finalPath); for (let i 0; i Number(total); i) { const chunkPath path.join(chunkDir, String(i)); const ivAndCipher fs.readFileSync(chunkPath); const iv ivAndCipher.slice(0, 12); const cipher ivAndCipher.slice(12); const decipher crypto.createDecipheriv(aes-256-gcm, key, iv); // 这里要补上加密时的 AADAAD 是 fingerprint:index 的字节形式 decipher.setAAD(Buffer.from(${fingerprint}:${i})); const decrypted Buffer.concat([ decipher.update(cipher), decipher.final(), ]); writeStream.write(decrypted); } writeStream.end(); writeStream.on(finish, () { fs.rmSync(chunkDir, { recursive: true, force: true }); res.json({ url: /files/${fingerprint}.bin, size }); }); });看到这里你应该能明白核心逻辑并不复杂复杂的是前后端对分片元数据的约定要保持一致。尤其是 AAD如果前端加密时用了fingerprint:index后端解密时必须一模一样连中间那个冒号都不能换。4.3 怎么把加密续传塞进现有上传组件很多人不会从零写上传组件项目里可能已经用了现成的上传库。这种情况下怎么集成加密和断点续传我的答案是不要动上传库的分片逻辑用自己的代码包一层把加密后的数据喂给上传库或者反过来在上传库的回调里加加密处理。比如现有的上传库提供了onFileSelected、onProgress、onSuccess这些回调你就可以在文件选择后先算指纹然后把“加密后的分片 Blob”作为实际上传的数据源。上传库只管“把 Blob 传到指定接口”它并不知道传的是密文。续传时你在上传前调用查询接口过滤掉已上传的分片只把缺失部分塞给上传库。如果上传库本身自带断点续传问题就稍微复杂一点。它可能用自己的本地缓存记录分片状态状态里存的是分片序号和 Blob。你需要在加密后重新生成一份 Blob并让上传库的缓存记录“以加密后的 Blob 为准”。否则断点恢复时它可能会尝试上传旧的明文分片后端一查哈希不对就会拒绝。提示如果上传库的缓存机制不可控最省事的方案是禁用上传库自带的断点续传完全用我们自己这套指纹后端查询方案来控制。上传库只承担“单次分片上传”的能力。这样可以避免两套续传逻辑互相打架。5. 实战中的坑与排查思路5.1 高频问题速查表我在实际测试过程中收集了一批踩过的坑做成表格放在这里按问题、原因、解决方式三列整理方便你对照排查。问题原因解决方式合并后的文件打不开文件头是乱码加密后没解密直接拼接或者解密时 AAD 不一致确认前后端 AAD 完全一致加密/解密使用相同密钥和算法参数断点续传后文件损坏部分内容重复后端合并时按字母序排序文件1 和 10 排错位改为按数字序号排序parseInt 后再比较刷新后秒传成功但文件只有 0 字节查询接口只看指纹没看文件大小查询接口必须校验文件大小指纹相同且大小一致才能秒传加密后上传速度明显变慢每个分片都用arrayBuffer()把 Blob 读进内存再加密小分片可以接受超大文件考虑 Web Worker 异步处理避免阻塞 UI并发 3 个分片时出现 503后端临时目录无写入权限或磁盘空间不足调整临时目录权限监控磁盘占用前端报crypto.subtleundefined当前页面不是 HTTPS或浏览器不支持 Web Crypto API确保 HTTPS 环境如果是本地调试用 localhost 环境合并时内存溢出了把整个文件读进内存再解密拼接改用流式写入逐分片解密后写入见前面 merge 代码续传时提示分片数超过预期后端临时目录残留了上一次失败任务的脏数据在查询接口增加清理逻辑比如超过 2 小时未完成就二次确认后清掉这个表格覆盖了我遇到过的大部分问题。还有一个不常见但很坑的情况文件大小刚好是分片大小的整数倍时前端createChunks不会多切最后一片但后端判断分片完整时容易把最后一个分片漏掉。建议切分时用Math.ceil并且合并前检查分片序号是否为0 到 total-1全覆盖。5.2 几条实操建议与习惯先说断点续传的本地缓存。除了后端记录已上传分片我还会在前端把分片状态写进 localStorage。这样做有两个好处一是后端查询接口压力小一点二是用户可以立刻看到哪些分片还没传不用等接口返回。但注意 localStorage 有容量限制不能存大数组只存序号列表和文件指纹足够了。如果你要存的信息多一点我建议切换到 IndexedDB容量大得多且不会在浏览器退出时丢失。再说分片上传的进度计算。单片传输进度可以靠 XMLHttpRequest 的upload.onprogress但是多片并发时总进度不能简单把单片进度相加。我常用的算法是已分片数 / 总分片数 × 0.9 当前活跃分片平均进度 × 0.1。这样保证整体进度在 90% 以前主要反映分片完成情况最后 10% 留给合并接口的等待时间用户看到进度条走到 99% 后不再前进也不会太焦虑。加密方面还有一个建议如果不需要端到端加密我其实更推荐“HTTPS 服务端存储加密”的组合。前端加密并不是免费的它增加了 CPU 计算量移动端尤其明显而且会带来密钥管理复杂度。只有当业务有明确的数据私密性要求或者安全合规强制要求“数据不得以明文形式经过应用层”才值得在前端做这一层。最后提醒一句密钥不要写死在 JS 里。如果你在代码里看到const KEY 1234567890abcdef那就是在裸奔。换用后端下发每次上传任务独立密钥到期自动失效这应该是底线。这些经验是反复踩坑之后才沉淀下来的。我处理的模拟项目里刚开始加加密的时候就栽在“整体加密还是分片加密”的选择上导致续传功能一度无法实现。后来改成每片独立加密配合 AAD整个逻辑一下子清晰了前后端联调也顺利很多。你如果正在做类似的东西建议先按这个流程走通一个最小版本再用真实大文件压测比一上来就堆代码高效得多。
📝

华诺云谱内容团队

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

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

你可能需要的服务

订阅华诺云谱资讯周报

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

↑