WebUploader深度改造:超大附件分片断点续传插件实战

📅 发布时间:2026/9/8 2:36:45
WebUploader深度改造:超大附件分片断点续传插件实战 做军工信息化相关的系统最头疼的功能清单里“大文件上传”绝对排前三。用户给你一个几个GB的卫星视频要求在内网环境传到服务器浏览器还是五花八门的老版本网络偶尔还给你来个中断。常规的上传组件根本扛不住Flash方案已经彻底废了纯自研又费时费力。最后我选择基于WebUploader做了一次深入改造封装成一个跨浏览器的超大附件分片断点续传插件。这篇就把整个改造思路和落地过程完整复盘一遍包括分片策略、断点续传机制、前后端接口约定以及军工内网环境下必须处理的浏览器兼容和国密算法问题给准备搞大文件上传又不想从零造轮子的同学一份可直接参考的方案。1. 项目背景为什么WebUploader还需要二次改造1.1 卫星视频根本不是“普通大文件”先明确一下业务场景。我们在做的是一套面向军工行业的视频资料管理系统用户上传的素材主要是卫星视频数据单文件动辄几个GB到几十个GB格式包括MP4、TS、MXF这类专业封装。之前用的方案是JSP页面里嵌入ActiveX控件上传但ActiveX只能在IE下运行浏览器一升级整个功能就瘫痪运维压力非常大。后来考虑用原生HTML5实现但时间紧、兼容性要求又高最终决定在WebUploader基础上做改造。WebUploader是百度开源的老牌文件上传组件优点很明显支持分片上传、并发控制、拖拽上传、进度回调API设计成熟而且底层自动封装了HTML5和Flash两套Runtime。不过这里有个必须说清楚的前提Flash Runtime现在已经不能用了Adobe在2020年底就停止了维护军工内网更不可能允许Flash这种有严重安全漏洞的组件存活。所以我们的改造目标非常明确只用HTML5 Runtime不支持老掉牙的IE6/IE7/IE8但必须兼容IE11、Chrome 45以上、以及国产化浏览器比如基于Chromium内核的奇安信、360企业版等。1.2 超大附件上传的四个核心痛点积累了这几年做大文件上传的经验我认为所谓的“超大附件”难点其实就四个文件切分、断点续传、并发控制、数据完整性。第一是文件切分。浏览器虽然可以通过Blob.slice把文件切成小块但不能无脑切切大了单请求容易超时、切小了会产生成千上万个小文件服务端存起来都费劲。第二是断点续传。几GB的视频传了一半网络断了如果让用户重新传这种体验在军工行业是绝对不行的用户不会骂产品经理会直接打电话骂开发。第三是并发控制。一次性把所有分片都发出去浏览器崩溃不说服务端也扛不住必须限制并发线程数。第四是数据完整性。军工行业对数据出错的容忍度极低视频文件必须保证分片上传完成后合并出来的文件和源文件完全一致哈希校验、国密算法这些都不能少。1.3 跨浏览器的真实含义标题里说的“跨浏览器”在军工内网场景下跟互联网公司理解的完全不是一回事。互联网公司只要兼容Chrome、Firefox、Safari的最新版就行而军工内网由于安全策略和采购渠道的限制浏览器版本往往滞后很多年。有的办公电脑还在用IE11有的单位强制使用国产化终端和国产浏览器还有的单位用的定制版Chrome内核浏览器屏蔽了CDN域名导致外部JS加载不出来。所以这次改造要解决的跨浏览器兼容具体来说就是支持IE11放弃IE10以下、支持基于Chromium 45内核的各类国产浏览器、支持标准的Chrome和Firefox同时所有静态资源必须本地化部署不能依赖任何公网CDN。在保证这些兼容性的前提下再把分片上传和断点续传做到插件里。2. 总体设计分片与断点续传的核心机制2.1 分片策略大小怎么定、并发开多少分片是整个方案的地基。我的经验是分片大小要根据实际网络情况来定不能拍脑袋。一般有两种典型场景一种是军工内网专线带宽高、延迟低分片可以适当大一些50MB甚至100MB都能接受另一种是远程站点通过加密链路传数据带宽小还容易抖动这时候分片必须小5MB左右比较稳。具体计算可以参考这个思路假设峰值带宽是10MB/s一个分片在网上的传输时间控制在1到3秒之间比较理想那分片大小就在10MB到30MB之间。如果带宽只有2MB/s分片还设成50MB传一个分片要25秒中间任何一次网络抖动都可能导致分片失败失败重传的成本非常高。如果带宽很大但分片设成1MB几GB文件会产生几千个分片服务端文件句柄和Redis记录都会爆炸。我在这套插件里的默认配置是单分片10MB、并发线程3。为什么并发开3而不是10因为卫星视频文件通常不止一个用户同时传如果每个用户开10个并发服务端同时要处理几十个分片写入磁盘I/O很容易成为瓶颈。而且WebUploader的并发线程数其实是指同一文件的分片并发数3个线程配合自动重试实测在10MB分片下能把内网带宽跑满同时服务端压力也在可控范围。2.2 断点续传的三要素断点续传说起来很简单就是“已经传过的分片不再传第二次”但真正实现要解决三个问题怎么识别同一个文件、怎么知道哪些分片传过了、怎么在服务端把分片拼回去。文件识别我采用“文件指纹”方案。对大文件做全量MD5耗时太长几GB文件算一次MD5可能要几十秒用户体验很差。实际项目里我采用“抽样指纹”取文件开头1MB、中间1MB、结尾1MB加上文件大小拼成字符串后用MD5或国密SM3计算出一个指纹。这种方案的碰撞概率在超大文件场景下可以接受而且计算速度非常快基本秒出。已传分片的记录我做了“双端存储”。前端通过localStorage存储每个文件指纹对应的已上传分片索引数组用户刷新页面后还能读取到服务端通过Redis存储同样的分片索引集合。每次上传分片前前端先向服务端查询该文件指纹下哪些分片已经存在返回后直接跳过这些分片。最后是服务端合并。全部片传完后前端调用合并接口服务端按照分片索引顺序拼接文件。拼接的时候要注意并发写入问题我采用的是所有分片先落盘到临时目录合并时按索引顺序读取并写入最终文件合并完成后计算整个文件的SM3哈希和前端提交的哈希进行比对一致才认为上传成功。2.3 为什么选用WebUploader而非完全自研有一部分人说可以完全自研我承认纯封装XMLHttpRequest加Blob切片并不难但工程落地时你会发现需要处理大量边缘场景文件重复选择、上传队列管理、拖拽上传的样式处理、进度条动画、文件类型过滤、多文件并行队列等等。WebUploader把这些都做好了我们只需要在它的事件机制上做二次开发把分片、断点、合并这些业务逻辑挂进去就行。还有一点是WebUploader的插件机制。它本身支持通过WebUploader.Uploader.register注册插件方法扩展起来很舒服。我最终交付的不是一个改得面目全非的Demo页面而是一个独立的JS文件其他系统只要引入WebUploader核心库加这个插件文件再写几行初始化代码就能获得完整的分片断点续传能力。这就是标题里说的“插件”的含义。3. 插件化改造实操WebUploader JS改造全过程3.1 初始化参数与关键配置我把整个改造封装成一个名为SatVideoUploader的插件外部调用方式很简单var uploader new SatVideoUploader({ pick: #pickBtn, dnd: #dndArea, accept: { title: 视频文件, extensions: mp4,ts,mxf,avi,mov }, server: /api/upload/chunk, chunkSize: 10 * 1024 * 1024, threads: 3, auto: false, formData: function() { return { token: getToken() }; } }); uploader.on(uploadComplete, function(result) { // 上传完成回调 console.log(result.fileKey, result.fileName); });内部初始化WebUploader时有几个参数必须特别注意。第一个是chunked: true开启分片模式。第二个是chunkSize我们设为10MB。第三个是threads: 3并发线程数不要调太高。第四个是fileVal设置分片上传时的字段名这套方案里我用的是chunk。还要加一个duplicate: true允许重复选择同一个文件因为断点续传场景下用户可能重复选择同一个文件来继续上次的上传进度。有一个参数容易被忽略prepareNextFile。平台默认情况下WebUploader会在一个文件正在上传时自动准备下一个文件但分片断点续传场景下这会导致内存占用过高。我建议显式设置为false让用户手动触发上传避免多个大文件同时加载到内存。3.2 核心代码断点续传状态的读写断点续传的完整实现贯穿WebUploader的几个关键事件。核心逻辑在uploadBeforeSend事件中每次发送分片之前先判断该分片是否已经上传过如果是直接标记跳过。uploader.on(uploadBeforeSend, function(block, data) { var file block.file; var fileKey getFileKey(file); var uploadedChunks getUploadedChunks(fileKey); data.fileKey fileKey; data.chunkIndex block.chunk; data.chunkTotal block.chunks; data.fileName file.name; data.fileSize file.size; data.fileHash fileHashCache[fileKey]; if (uploadedChunks.indexOf(block.chunk) 0) { block.skip true; skipChunkCount; } });这里的getFileKey就是我前面说的抽样指纹getUploadedChunks从localStorage读取已上传分片数组。WebUploader会在发送前检测到block.skip属性如果为true就不再发送该分片对应的HTTP请求。这一步是整个断点续传能成立的基石没有它前面所有的本地记录都是摆设。分片上传成功后需要把当前分片索引写入本地记录同时准备触发合并操作uploader.on(uploadSuccess, function(file, response) { var fileKey response.fileKey; var chunkIndex response.chunkIndex; if (fileKey) { markChunkUploaded(fileKey, chunkIndex); } if (response.complete) { requestMerge(response.fileKey, response.fileName); } });服务端在每次分片上传时都会返回当前文件的已上传分片总数和维护合并状态。当服务端检测到所有分片都已上传时会在响应里标记complete: true前端拿到这个标记后直接触发合并请求不需要自己数分片数量这样能避免“前端以为传完了、服务端其实还缺分片”的时序坑。3.3 暂停、恢复与取消的实现WebUploader原生并没有提供完整的暂停/恢复API只有stop()和upload()这两个方法。实际项目中我通过组合这两个方法实现了控制能力。暂停时调用uploader.stop(true)这个true表示停止后清空任务队列避免恢复时重复入队。恢复时重新调用uploader.upload()这时每个分片在发送前都会执行uploadBeforeSend判断已经传过的分片会被自动跳过只继续传未完成的分片。整个过程不需要额外维护复杂的队列状态。取消上传就更直接了调用uploader.removeFile(file)同时把本地存储中的该文件指纹记录清掉。这里容易踩一个坑如果不清除记录用户取消后重新选择同一个文件插件会误判为“断点续传”而跳过所有已传分片但此时服务端分片可能早已被清理规则删掉最后会陷入死循环。所以取消时一定要同步清除服务端的临时分片记录我一般是先调用服务端的abort接口再执行前端清理。3.4 大文件指纹与抽样哈希实现文件指纹的代码需要结合抽样策略来写。由于浏览器没有直接读取任意偏移量的同步API我使用FileReader配合Blob.slice来读取指定片段function getFileKey(file, callback) { var CHUNK_SIZE 1024 * 1024; // 抽样片段大小 1MB var fileSize file.size; if (fileSize 3 * CHUNK_SIZE) { readSlice(file, 0, fileSize, function(buffer) { callback(calcHash(buffer)); }); return; } var promises []; // 头部 promises.push(readSlicePromise(file, 0, CHUNK_SIZE)); // 中间 var midStart Math.floor(fileSize / 2) - Math.floor(CHUNK_SIZE / 2); promises.push(readSlicePromise(file, midStart, CHUNK_SIZE)); // 尾部 promises.push(readSlicePromise(file, fileSize - CHUNK_SIZE, CHUNK_SIZE)); Promise.all(promises).then(function(results) { var combined new Blob(results); readSlice(combined, 0, combined.size, function(buffer) { callback(calcHash(buffer)); }); }); }这里要注意promises.push的顺序就是读取顺序头部、中间、尾部各1MB组合成一个3MB的Blob后再计算哈希。用Promise.all而不是逐个串行读是为了减少整体耗时。实测下来一个10GB的文件生成指纹只需要1到2秒用户基本无感知。还有一个细节在军工内网环境下如果对哈希算法有合规要求可以将calcHash函数替换为国密SM3实现。Web Crypto API原生不支持SM3但可以使用asm.js或WebAssembly版本的国密库。在我的插件里我把哈希算法做成了可配置项默认使用MD5或SHA-256配置为sm3时自动加载国密模块。4. 前后端接口约定与合并策略4.1 分片上传接口设计前端改造只是整个链路的一半服务端接口必须和前端完全对齐。我这边配套使用Java开发的服务端接口约定如下参数类型说明chunkMultipartFile分片文件二进制内容fileKeyString文件指纹由前端抽样哈希生成chunkIndexInteger当前分片索引从0开始chunkTotalInteger总分片数量fileNameString原始文件名fileSizeLong文件总大小fileHashString合并后完整文件的哈希值分片保存接口的处理逻辑主要有三步校验参数合法性、将分片保存到以fileKey命名的临时目录、更新Redis中该文件的分片索引集合。PostMapping(/api/upload/chunk) public Result uploadChunk(RequestParam(chunk) MultipartFile chunk, RequestParam(fileKey) String fileKey, RequestParam(chunkIndex) Integer chunkIndex, RequestParam(chunkTotal) Integer chunkTotal, RequestParam(fileName) String fileName, RequestParam(fileSize) Long fileSize, RequestParam(fileHash) String fileHash) throws IOException { // 1. 参数校验fileKey是否存在、chunkIndex是否在合法范围 if (chunkIndex 0 || chunkIndex chunkTotal) { return Result.error(分片索引非法); } // 2. 保存分片到临时目录 String shardPath shardRootDir File.separator fileKey; File shardDir new File(shardPath); if (!shardDir.exists()) { shardDir.mkdirs(); } chunk.transferTo(new File(shardDir, chunkIndex .part)); // 3. 更新Redis已上传分片集合 String redisKey upload:chunks: fileKey; SetOperationsString, Integer ops redisTemplate.opsForSet(); ops.add(redisKey, chunkIndex); int uploadedCount ops.size(redisKey).intValue(); // 4. 返回合并状态 MapString, Object data new HashMap(); data.put(fileKey, fileKey); data.put(chunkIndex, chunkIndex); data.put(complete, uploadedCount chunkTotal); return Result.ok(data); }这里有一个容易踩的坑MultipartFile.transferTo()在部分国产中间件上会抛出跨目录限制异常。我最初在WebLogic上部署测试时这个方法一直报FileNotFoundException排查了半天发现是WebLogic对临时目录的访问权限做了限制。后来改成先getInputStream()再自己写文件采用Files.copy()的方式这个问题才彻底解决。在军工内网常用的一些老旧应用服务器上这类兼容问题极其常见。4.2 合并接口的并发安全合并接口做两件事按索引顺序拼接文件、计算合并后文件哈希并与前端提交的fileHash比对。PostMapping(/api/upload/merge) public Result merge(RequestParam(fileKey) String fileKey, RequestParam(fileName) String fileName, RequestParam(fileHash) String fileHash) throws IOException { String shardDir shardRootDir File.separator fileKey; File target new File(finalDir File.separator generateStoredFileName(fileName)); // 按索引顺序读取所有分片并写入目标文件 SetInteger uploaded redisTemplate.opsForSet().members(upload:chunks: fileKey); ListInteger sortedChunks new ArrayList(uploaded); Collections.sort(sortedChunks); // 排序是关键顺序错乱会导致视频文件损坏 try (OutputStream out new BufferedOutputStream(new FileOutputStream(target))) { for (Integer index : sortedChunks) { File part new File(shardDir, index .part); Files.copy(part.toPath(), out); } } // 计算合并后的SM3哈希并与前端提交的fileHash比对 String actualHash sm3Digest(target); if (!actualHash.equals(fileHash)) { target.delete(); return Result.error(文件完整性校验失败请重新上传); } // 清理临时目录和Redis记录 FileUtils.deleteDirectory(new File(shardDir)); redisTemplate.delete(upload:chunks: fileKey); return Result.ok(上传成功); }合并过程最核心的就是分片排序。前端虽然是按索引顺序发送的但并发线程3的情况下很多分片几乎是同时到达服务端的如果服务端为了省事直接把分片追加写入目标文件那顺序就会错乱最终合并出的视频文件会出现花屏、卡帧甚至完全无法播放。所以我选择先落盘再合并的方式宁可多占一些磁盘空间也要保证顺序完全正确。合并接口还必须考虑幂等性。如果前端因为网络原因调了两次合并接口第二次调用时临时目录可能已经不存在了这时应该返回已上传成功的结果而不是报错。我在代码里加了一层判断如果目标文件已经存在且哈希一致直接返回成功。4.3 超大文件的上传记录清理策略军工内网的服务器磁盘往往有限大量的几GB分片临时文件如果不及时清理很快就会把磁盘塞满。我定了一套清理策略分片临时目录保留24小时超过时间且未进入合并阶段的自动清理Redis记录过期时间设为48小时每天凌晨通过定时任务扫描临时目录删除过期文件。需要注意一个细节清理策略不能太激进。用户可能在第一天上传了一半第二天继续传。如果不到24小时就清理掉分片文件断点续传能力就直接作废了。我的经验是至少在业务要求的“最大暂停时长”上加一倍作为临时文件的保留期限。5. 军工场景下的浏览器兼容与安全处理5.1 兼容IE11与国产浏览器的技巧插件在标准Chrome上跑通只是第一步真正的考验在IE11和国产浏览器上。我总结几个必须处理的点。第一是File.slice方法的兼容。IE10以上的File对象支持slice但部分IE11版本对File.prototype.slice的稳定性和大数据量切片的性能并不理想。WebUploader内部做了一个兼容封装它优先使用File.prototype.slice如果检测到File.prototype.mozSlice或File.prototype.webkitSlice也会使用。对于IE11它走的是原生slice。我建议不要直接修改WebUploader底层而是在选择文件后先做一次能力检测如果不支持分片直接提示用户更换浏览器而不是让用户看到一个永远卡在0%的进度条。第二是localStorage的可用性。IE11还有一个残酷的Bug如果用户关闭了浏览器隐私模式localStorage会变成只读状态setItem会抛异常。所以封装插件时必须对localStorage的读写做try-catch异常情况下降级为内存记录——虽然刷新后断点续传能力会丢失但至少不会让整个上传功能崩溃。第三是网络请求的问题。很多国产浏览器的内核安全策略比较独特会默认阻止携带Content-Type: multipart/form-data的POST请求出现跨域情况。在军工内网里前端页面和应用后端通常部署在同一个域名下这个问题不明显。但如果是分域名部署一定要在服务端正确配置跨域响应头Access-Control-Allow-Origin同时前端要设置withCredentials属性否则上传请求会在浏览器里被静默拦截表现为分片“永远传不上去”。5.2 静态资源本地化与离线部署军工内网通常是完全离线的WebUploader自带的CSS和JS资源不能通过公网CDN加载。我专门建了一个静态资源目录把WebUploader的核心JS、中文语言包、CSS样式、以及图片素材全部打进安装包里。插件对外只暴露一个dist目录集成方把整个dist拷贝到自己的新项目里就行。这里要说一个典型坑WebUploader的源码注释里包含了许多外部链接个别安全审查工具会检测出这些外链并报“数据外传风险”。这些其实只是一段注释并不会真正发请求但为了过安全评审我直接用发布版压缩文件替换了源码版压缩后的JS中注释已经被webpack抹掉风险提示自然消失。5.3 国密算法与数据完整性校验军工行业的数据安全要求远高于普通企业传输链路加密和数据完整性校验是标配。我在插件里做了两层防护。第一层是分片内容完整性。前端在计算文件抽样指纹后还会为每个分片计算独立的SM3哈希值并在发送分片时把当前分片的哈希传给服务端。服务端在保存分片之前会重新计算接收到的分片内容的SM3哈希并与前端提交的值比对不一致直接拒绝。这样能保证每个分片在传输过程中没有被篡改或损坏。第二层是合并后的文件级完整性。合并完成后服务端计算整个文件的SM3哈希与前端在fileHash参数中提交的哈希进行比对。由于前端提交的fileHash是在选择文件后就固定下来的全程哈希所以这个比对能确认合并后的文件与源文件完全一致。前端计算全程哈希是通过抽样方式但合并后校验如果要求更严格也可以让服务端在合并完成后把完整的SM3哈希单独传给前端约定好的回调由前端决定是否更新业务系统里的档案记录。需要强调的是这里用的是“哈希校验”来防传输错误并不是防恶意篡改的加解密。如果业务确实需要对分片内容做加密传输可以在分片发送前使用国密SM4算法对分片字节进行加密服务端解密后再保存。我这套方案考虑到性能优先默认不启用力但在插件里预留了encrypt: true的配置项需要时接入即可。6. 常见问题与排查实录6.1 断点续传失效排查在实际运行中断点续传失效是最常见的工单问题。现象是用户上传到一半刷新页面或关闭浏览器后重新打开点击同一个文件插件没有跳过已传分片而是从头开始传。排查思路先看localStorage。打开浏览器开发者工具查看当前域名下的Local Storage确认是否存在以upload:progress:开头的记录。如果没有说明前端没有成功写入记录这时要检查是不是浏览器隐私模式导致的localStorage只读或者存储写入时抛了异常被catch吞掉了。如果前端记录正常再看Redis。用Redis客户端查看upload:chunks:{fileKey}键是否存在以及它的成员数量。如果Redis中对应的分片集合已经不存在或为空那很可能是定时清理策略把记录删了。我推测这种情况多发生在“用户当天传了一半第二天才续传”的场景48小时的TTL恰好到期。最后还要检查一个容易忽略的点文件指纹的一致性。如果用户选择了两个不同路径但同名同大小的文件或者系统在指纹计算时用了包含绝对路径的元数据导致key变化断点续传也会失效。我建议指纹计算时只用文件名、大小、抽样片段内容这些内容属性不要掺入路径信息。6.2 浏览器崩溃与内存溢出上传几个GB的文件时浏览器内存占用会持续攀升严重的直接崩溃。这个问题要从两个维度解决。一是控制分片并发数。threads不要盲目调大4个并发已经很多了实际测试中3个并发最稳妥既能利用带宽又不会把浏览器内存吃满。二是及时释放File对象的引用。WebUploader在文件上传完成后会把文件对象保留在队列中这对超大文件来说就是几个GB的内存泄漏。我改造时在uploadComplete事件里手动调用uploader.removeFile(file)让垃圾回收尽早回收大文件的内存占用。chunkSize也会影响内存。分片发送时浏览器需要把分片读入内存10MB的分片意味着每个并发线程最多占10MB左右的临时内存3个线程就是30MB还算合理。但如果把chunkSize调到100MB三个并发同时加载就可能有300MB内存占用在低配办公电脑上很容易触发崩溃。还有一个细节拖拽上传时浏览器会先把整个文件读入内存。对于几GB的视频拖拽上传可能会直接导致页面无响应。我改造后的插件在检测到文件大于2GB时会提示用户使用文件选择框而不是拖拽上传这是一个非常实用的避坑经验。6.3 分片上传卡住不动用户反馈“进度条卡在某个百分比不动”这种情况最先怀疑网络问题但军工内网的网络往往很稳定更多是因为服务端并发处理不过来。分片上传请求是短连接服务端如果不小心在接口里加了全局锁并发3个线程会变成串行处理进度看起来就像卡住了一样。我用arthas诊断过一次类似问题发现服务端接口里有一个全局级synchronized代码块把分片写入Redis的集合操作锁住了导致并发请求排队。解决办法是把锁的粒度缩小只对同一个fileKey的分片写入加锁或者直接使用Redis的原子集合操作不再加应用层锁。改完后上传进度立即恢复流畅。还有一个前端层面的原因WebUploader默认的失败重试次数是0次分片请求一旦失败就直接标记为失败但用户感知是“卡住不动”。我建议在插件初始化时配置retry: 3并监听uploadError事件遇到临时网络错误做自动重试。注意重试不能无限次不然服务器持续故障时前端会疯狂发送请求把服务端彻底打挂。6.4 上传速度优化心得最后聊几个实测有效的提速技巧。第一用uploadProgress事件里返回的speed字段做实时测速如果发现连续几秒速度低于某个阈值自动把并发线程数降为1减少无效请求。第二把分片大小和系统MTU对齐内网传输环境MTU一般是1500字节分片大小是10MB约7000个包实际传下来效率最高。第三服务端保存分片时用SSD临时目录而不是机械硬盘大文件合并的性能瓶颈几乎都在磁盘I/O。我们最初把分片临时目录放在机械硬盘上3个并发时服务端CPU占用不高但合并且等待时间翻了三倍后来换成SSD后整个流程快得让人怀疑之前是不是代码写错了。还有一个容易被忽略的点浏览器本身对同一域名下的并发连接数有限制。HTTP/1.1下Chrome对同一个域名的最大并发连接数是6我们插件用3个线程传分片页面还有资源请求和其他AJAX请求加起来很容易触发浏览器连接限制。如果上传速度一直提不上去可以让运维在后端加一层Nginx开启HTTP/2支持或者把上传接口单独部署一个域名都能明显改善并发性能。我在实际部署中就采用了上传接口独立端口的方式成功的把上传速度从20MB/s提高到了50MB/s以上。7. 最后的补充与个人体会整个改造过程持续了大概三周最深的体会有两点。一是不要迷信“从头造轮子”。WebUploader虽然看起来老但它的分片队列模型和事件机制写得很扎实比很多年轻人从零写的上传组件成熟得多。在WebUploader上做扩展远比在原生XMLHttpRequest基础上重新应对各种浏览器细节更靠谱。二是“断点续传”四个字里“断点”是前端问题“续传”是前后端协同问题。只在前端做localStorage记录服务端不配合保留分片续传就是空谈反过来服务端做得再完善前端没有可靠的本地状态记录也不知道从哪里继续传。所以做这种功能一定要想着前后端一体设计接口约定写清楚分片排序、哈希校验、临时文件清理这些细节都得两边同时对齐。如果后续有继续扩展的机会我会在插件里加入多文件级别的任务调度比如多个大文件同时上传时的资源分配策略以及上传完成后的秒传能力——服务端发现fileKey对应的完整文件已经存在时直接跳过所有分片返回秒传成功。这些能力在目前的版本里已经有雏形只是没有完全打磨成熟。这套方案上线后最让我欣慰的是用户真的不再抱怨上传问题了。一个十几GB的卫星视频内网环境稳定时大约十分钟传完中途断网、刷新、换电脑都能接着原来的进度传。希望这份实操记录对正在处理类似大文件上传需求的人有用。