资讯动态

基于WebUploader的大文件分片断点续传插件深度改造实践

发布时间:2026/9/10 5:07:01 来源:尧图企业网站定制
做军工行业地面站卫星视频归档的时候最头疼的就是上传那一环。视频文件动不动就几个GB到几十个GB内网带宽还忽高忽低传到一半断了就得从头再来领导和甲方盯着进度烦得很。我当时的做法是基于WebUploader做了一次深度改造把它封装成一个真正可用的大文件分片断点续传插件。这个插件从设计到落地踩了不少坑也沉淀了一套可以复用到其他场景的方案。这篇就完整拆解一下整个改造思路和关键实现包含跨浏览器兼容处理、分片策略、断点续传机制和插件架构设计希望能给同样被大文件上传折磨的人一点参考。1. 需求拆解与插件总体思路1.1 卫星视频上传的真实场景先说说实际业务长什么样。地面站接收卫星下传的视频数据后需要把原始视频文件从站端设备上传到数据中心做后续的人工智能识别、编目和存储归档。这个数据链路非常特殊第一文件极大单段任务原始视频通常在2GB到50GB之间某些连续观测任务甚至能到上百GB第二网络环境差现场往往是专线或微波链路虽然内网隔离但延时高、丢包率不稳定断传是常态第三基本都是国产化软硬件环境浏览器可能是老版本的Chromium内核、IE11、甚至政务内网定制的国产浏览器HTML5新特性支持不完整你不能只赌一个完美环境。我最初试着直接用WebUploader默认配置传一个6GB文件结果传了三个小时中途一个网络抖动直接回到起点非常痛。由此确定需求文件分片上传、任意分片失败可重传、断网或刷新页面后可恢复进度、不同浏览器行为一致、最好能秒传重复文件。1.2 为什么选WebUploader作为底座市面上有大文件上传方案很多比如plupload、resumable.js、vue-simple-uploader还有后端的MinIO分片接口。但WebUploader有一个别家不太比得上的优势它的分片和file切片逻辑非常成熟自带多线程多实例并发控制、文件MD5计算、队列管理、事件机制而且对旧浏览器的兼容策略很早就有一套历史上用Flash降级虽然现在Flash已死但它的HTML5降级架构仍然可以借鉴。在军工内网这种“不能用最新酷炫特性”的环境里WebUploader的老成持重反而成了优点。不过WebUploader本身也不是为“超大文件断点续传”设计的。它默认的分片是一次性传完服务端按分片合并即可但没有记录“哪些分片已经传过了”的机制。所以我的思路是保留它已有的文件解析、分片、并发上传、事件回调这套核心在它的外层封装一个带元数据登记、断点状态恢复、服务端校验能力的插件层。这样既不用推翻重写又能满足军工行业的特殊要求。1.3 插件整体架构设计整个插件我命名为UploaderPro逻辑上分成四层核心层基于WebUploader的初始化、文件队列、分片上传执行这层完全复用WebUploader的能力。扩展层自定义插件模块负责监听WebUploader的事件拦截分片上传的before-send流程注入断点校验、自定义请求头、文件指纹等。存储层把已上传分片信息、文件MD5、上传进度快照存入localStorage或IndexedDB按文件ID维度做映射方便恢复。服务端对接层统一封装后端接口协议包含“文件登记—分片校验—分片上传—分片合并—完成确认”五个动作。这样分完之后每一层职责清晰测试也好写后续如果要迁移到其他上传组件或者换成原生XMLHttpRequest只需要替换调用层扩展层和存储层可以继续复用。2. 分片、秒传与断点续传核心机制2.1 分片大小到底怎么定分片大小是第一个要拍板的参数直接影响上传成功率和并发效率。我在实际测试中发现分片太小会导致请求数量爆炸一个50GB文件如果按1MB分片会产生51200个HTTP请求光是请求头消耗都受不了分片太大会失去断点续传的意义一个分片传一半失败还是要整片重传。军工内网链路的特点是带宽不低但延迟高、抖动明显我最终把默认分片设为8MB同时提供一个chunkSize配置项给不同站点调优。8MB这个值是有考量的在带宽100Mbps、RTT 50ms的内网环境8MB单分片传输时间约0.65秒HTTP请求头和响应头的开销占比很小如果RTT高到150ms分片可以适当放大到16MB减少来回次数。同时我建议限制并发数不要默认开5个以上。因为分片大了以后并发太多会打爆链路丢包重传反而拖慢整体速度。我实测在弱网下并发数设为3时吞吐量最高超过5后丢包率显著上升。chunkSize: 8 * 1024 * 1024, threads: 3,2.2 文件指纹与秒传实现断点续传的前提是要能稳定标识一个文件。我的方案是分两级指纹文件整体MD5和分片MD5。不要只算文件整体MD5因为一个上百GB的文件算整体MD5可能要几分钟等不起。更聪明的做法是“抽点计算 分片MD5确认”。具体逻辑是这样文件加入队列后先算每个分片的MD5同时每隔N个分片抽取一个作二次校验。上传前把该文件的全部分片MD5列表一次性提交给后端登记接口后端返回哪些分片已经存在可能来自其他用户或历史上传哪些需要重新上传。这样一来新文件第一次上传没有历史分片后端返回全部需要上传。上次传了一半的文件后端比对MD5列表返回缺失的分片编号前端只传缺失部分。完全相同的文件再次上传后端发现所有分片都已经存在直接标记合并实现秒传。这个方案比单纯靠文件名大小判断可靠得多。我遇到过两个不同任务生成了同样大小的文件但内容完全不同如果只靠名字和大小会误判成同一个文件导致数据损坏。MD5列表校验虽然多传了一些数据但在军工数据归档场景里正确性优先于效率。2.3 断点续传的上传记录与恢复断点续传最大的难点不是“能不能停下来”而是“停下来之后怎么知道传了哪些”。我设计了一个UploadRecordStore上传过程中的关键节点都会向它写快照{ fileId: 8a3f9c2e-1b5d-4f7a-9c1e-6b2d8a4f0e71, fileName: 2024-06-18_120000_sat_video.mkv, fileSize: 23622320128, overallMd5: 9e107d9d372bb6826bd81d3542a419d6, chunkSize: 8388608, totalChunks: 2816, uploadedChunks: [0, 1, 2, 5, 6], serverPath: /data/upload/archive/20240618/, timestamp: 1718700000000 }上传状态变更时uploadedChunks数组会更新。这里有个性能注意点当分片数量到了几千个时每次更新都写整个数组会卡顿我最后采用“批量累积更新 节流落盘”的策略内存里维护一个已上传分片的Set每成功一个就往Set里加每成功10个或者间隔5秒才把Set序列化写入localStorage。如果文件特别大、分片数上万建议改用IndexedDB因为localStorage有5MB上限几十GB文件的记录可能超过。恢复的时候页面加载完插件后会读取localStorage里的记录筛选出未完成的在文件列表中显示“继续上传”按钮。点击后不需要重新选择文件直接调用WebUploader的addFiles接口把记录关联到实际文件对象上然后进入断点校验逻辑只传缺失分片。2.4 失败分片的重试与网络恢复WebUploader原始的重试机制比较粗暴retry会重新加入队列但它对“哪个分片失败”没什么记忆。我要做的改造是监听uploadChunkFailed事件把失败的分片自动加入重试队列而不是让整个文件上传中断。我设置了一套重试策略每个分片失败后指数退避重试间隔从1秒开始最大延迟30秒。同一个分片连续失败超过3次暂停该分片所在的任务弹出网络异常提示但不清理已上传进度。监听online事件网络恢复后自动继续暂停的任务。服务端返回的响应如果包含chunkIndex前端会精确跳过错传失败的分片。这部分的细节直接决定了用户会不会骂人。我用一次真实场景验证过一个18GB文件在传了60%时现场交换机重启了一下所有网络连接被切断。WebUploader原始版本直接所有任务失败重启后从0开始改造后的插件在网络恢复后只补传了网络断开瞬间没传完的那几个分片然后继续从60%往下传最后总共只多花了三分钟就完成了剩余40%的任务。3. 插件化设计与跨浏览器兼容3.1 事件总线与扩展点设计要让这个插件在军工不同的站点之间复用不能把具体业务逻辑写死在代码里。我借鉴了轻量级插件系统的事件总线思想把整个上传生命周期拆成一系列钩子每个站点可以按需注入自己的逻辑。核心扩展点我用一张表来托管扩展点触发时机典型用途onFileAdded文件加入队列自定义文件名规则、加密标记beforeChunkUpload每个分片上传前注入Token、加密参数、校验服务端状态afterChunkUpload每个分片成功更新进度、上报日志onChunkFailed分片失败重试策略、告警通知beforeFileComplete全部传完准备合并引导服务端合并、验证文件长度onFileComplete合并完成更新业务状态、通知下游系统事件总线的实现其实很轻量用一个Map存储事件名到回调数组的映射配合一个简单的中介者模式就可以了。通过这个机制不同站点可以只通过配置JSON来启用或禁用某些扩展模块而不需要改插件源码。3.2 现代浏览器的原生分片方案在Chromium 80的现代内核上WebUploader底层使用的是Blob.slice()方法这个兼容性没有问题。我的插件在现代浏览器上可以直接使用完整的文件流处理包括FileReader读取二进制切片做MD5、XMLHttpRequest发送二进制数据、Blob对象直接上传。这里有一个特别要注意的点WebUploader在默认情况下分片数据是用File.slice(start, end)拿到的Blob然后调用formData.append(file, blob)上传。这种方式的缺点是MD5计算和上传各读取了一次文件大文件会占用大量内存和IO。我做了一层优化MD5计算时使用FileReader分片异步读取读完一片立即释放上传时再用slice直接取原始Blob。这样在两个阶段不会同时加载同一份数据减少了内存峰值。在我的实测中一个4GB文件在普通桌面电脑上MD5计算加上传的总内存占用控制在800MB以内相比一次加载整个文件的方案下降了约60%在工控机级别的国产硬件上也跑得动。3.3 老旧浏览器与国产浏览器的降级处理军工内网的老浏览器是个现实存在尤其是一些定制的安全浏览器和老的360企业版对Blob.slice的支持不完整甚至部分浏览器对FormData的二进制支持都有兼容问题。我针对这种情况做了三层降级第一层检测浏览器支持Blob.prototype.slice和FileReader如果支持走正常分片上传流程。第二层如果Blob.slice不可用但FileReader可用就用FileReader.readAsArrayBuffer手动切割ArrayBuffer然后用new Blob([buffer])包装成Blob再上传。这种方法兼容IE10性能略差但不影响功能。第三层如果浏览器连FormData上传二进制都支持不好那就退回模拟表单上传用隐藏iframe提交整个文件不进行分片。这是最后保底方案只在小文件场景可用一旦检测到这种情况我会在界面上提示“当前上传限制2GB以内”。此外服务端接口在设计时就要兼容分片上传和非分片上传两种模式。非分片模式上传时chunk字段固定为0服务端直接落盘即可。这样老旧浏览器用户虽然无法享受断点续传但至少能完成小文件传输。3.4 统一配置中心与按站点定制军工项目中不同站点的网络带宽、存储路径、认证方式、数据加密要求都不一样。为了不让插件变成一座孤岛我把所有可调参数收口到一个配置对象并在插件初始化时加载const uploader new UploaderPro({ container: #uploadPanel, chunkSize: 8 * 1024 * 1024, threads: 3, retryTimes: 3, maxFileSize: 100 * 1024 * 1024 * 1024, // 100GB serverUrl: { register: /api/upload/register, chunkCheck: /api/upload/check, chunkUpload: /api/upload/chunk, merge: /api/upload/merge }, authToken: () getCurrentUserToken(), encryptChunk: true, encryptType: sm4, // 国密SM4示例 storage: localStorage, // 或 indexedDB hooks: { onFileAdded: customRenameHook } });按站点定制的逻辑写在启动脚本里把具体配置传给构造函数。插件核心代码不感知站点差异只认配置和事件。这样在日后维护时一个插件包可以适配所有现场出问题也能快速定位是配置问题还是核心代码问题。4. 关键代码实现与落地过程4.1 WebUploader初始化与插件挂载改造的第一步是初始化原生的WebUploader并让它具备对外暴露核心能力的能力。我用了一个包装类把WebUploader实例藏起来只暴露自己封装的公共方法class UploaderPro { constructor(config) { this.config this._normalizeConfig(config); this._chunkStatusMap new Map(); this._initCore(); this._initStorage(); this._bindEvents(); } _initCore() { this.uploader WebUploader.create({ swf: this.config.swfUrl, server: this.config.serverUrl.chunkUpload, pick: this.config.pickElement, accept: this.config.accept || {}, auto: false, chunked: true, chunkSize: this.config.chunkSize, threads: this.config.threads, duplicate: true, formData: { token: this.config.authToken() } }); } }这里的核心是chunked: true。开启之后WebUploader会自动把文件切成chunkSize大小的分片并逐个上传它会自动在请求中附带chunk当前分片索引和chunks总分片数这样的参数就不用我们手动管理分片坐标了。4.2 分片MD5计算与文件登记分片MD5是整个断点续传的数据基础做不好后面全乱。我使用spark-md5这个库把文件切片后按顺序计算_uploader.on(beforeFileQueued, (file) { const fileId this._generateFileId(file); this._chunkStatusMap.set(file.id, { fileId, uploadedChunks: [], totalChunks: Math.ceil(file.size / this.config.chunkSize) }); // 如果本地已有上传记录说明是续传 const record this._storage.getRecord(fileId); if (record) { this._chunkStatusMap.get(file.id).uploadedChunks record.uploadedChunks; this._setRestoreMode(true); } return file; });在正式上传第一个分片之前必须先调用服务端登记接口把分片MD5列表发过去再根据返回结果决定哪些分片要传。服务端返回的是“待上传分片索引列表”前端根据这个列表去重排队async _registerFile(file, chunkMd5List) { const response await fetch(this.config.serverUrl.register, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fileId: file.fileId, fileName: file.name, fileSize: file.size, chunkMd5List }) }); const result await response.json(); // result.uploadedChunks: 服务端已有的分片索引 // result.missingChunks: 需要上传的分片索引 return result; }MD5计算对超大文件来说是一个耗时操作尤其是纯JS在低配工控机上跑。我建议界面显示“正在计算文件指纹预计耗时1分钟”的进度条而不是让用户以为卡死了。计算过程用requestAnimationFrame分批推进保证UI不冻结。4.3 服务端分片校验与合并服务端接口设计直接决定断点续传能不能成功。因为分片上传本质上是一个“无状态快递服务”每个分片都是独立的HTTP请求服务端必须能自主判断“这个分片要不要收”。我的后端接口设计如下登记接口POST /api/upload/register接收文件元数据与全部分片MD5在数据库创建上传任务记录返回已存在分片列表和缺失分片列表。这里的“已存在”包含两种情况历史未完成的任务中已上传的分片或者其他任务里MD5一致的分片。校验接口POST /api/upload/check接收文件ID和分片索引返回该分片是否已存在。前端在每次续传前会先校验剩余分片的状态避免盲目重传。上传接口POST /api/upload/chunk接收分片二进制数据和分片索引落盘到临时目录记录分片MD5到数据库。合并接口POST /api/upload/merge接收文件ID服务端检查所有分片是否齐全按索引顺序合并成完整文件并计算合并后文件的整体MD5与登记时的整体MD5比对。比对通过才标记完成不通过则返回缺失分片列表让前端补传。合并操作我建议放在服务端做而不是让前端把所有分片下载下来再合并。原因很简单前端合并虽然省了服务端的IO但超大文件会在浏览器里产生巨大的内存压力而且如果合并到一半浏览器崩溃状态会变得无法追踪。服务端合并可以保证原子操作和数据一致性。服务端合并的时候还有一个容易被忽略的细节分片大小不一致。最后一个分片往往比设定的chunkSize小合并时不能假设所有分片大小相同必须按“文件名_分片索引”的规则遍历用文件流按字节追加而不是用固定长度截取。4.4 进度上报与UI状态同步大文件上传时用户最关心的就是“传了多少、还剩多少、速度多少”。WebUploader默认的progress事件只报告当前文件的整体进度由已上传分片数量除以总分片数量计算得出这只是“已传输数据量”不是“文件可用进度”。我的改造方案是在上传界面展示三个维度总进度已传分片占比、当前速度最近10秒平均速度、剩余时间按当前速度预估。速度计算不能直接用瞬时值瞬时值波动太大我用一个环形缓冲保存最近5个采样点做滑动平均视觉上会稳很多。此外还有进度恢复的问题续传开始时总进度并不是从0开始而是从记录中的百分比恢复。这里我会弹一个提示“检测到未完成的上传任务正在恢复进度……”恢复完成后直接显示真实百分比不会让用户以为进度搞错了。4.5 加密分片与安全传输军工场景的数据安全等级比较高链路虽然内网隔离但为了保险起见我加入了分片级别的加密支持。前端在上传前对分片数据做SM4加密国密对称加密服务端收到后先解密再落盘。加密的字节处理有一个大坑加密之后的分片大小会变导致合并时无法直接按顺序拼接。我的做法是每一片都使用固定长度的加密块格式[16字节IV][加密数据]加密数据是原始分片的密文长度是16的倍数PKCS7填充。合并时服务端按顺序读取每一片单独解密后再写入目标文件。这样虽然多了些计算开销但保证了数据在传输过程中不会被旁路嗅探。另外加密操作尽量用Web Worker来做避免阻塞主线程。我实测一个64MB分片在普通PC上SM4加密大约需要800毫秒放到Worker里之后页面完全无感知。5. 军工内网场景的常见问题与性能优化5.1 常见问题排查速查表改造过程中积累了很多排查经验整理成表格供参考现象可能原因排查思路所有分片上传都失败Token过期或认证方式不兼容检查请求头中的Authorization字段看服务端日志是否有权限拒绝上传成功后合并超时分片过多或并发数量过大导致临时文件堆积增加服务端合并线程池大小或将每批合并分片数限制在500个以内断网恢复后进度回退localStorage记录没有及时落盘检查是否启用了节流落盘机制确认上传成功回调里有写入快照MD5计算阶段内存飙升一次性读取所有分片进行计算改为逐片读取并调用spark-md5的append计算完毕立即置空引用老浏览器点击上传按钮无响应文件选择控件被安全策略阻止或Flash组件缺失查看浏览器控制台报错必要时为老浏览器单独适配隐藏input方案分片上传后服务端只有一个文件服务端合并逻辑按固定长度截取而非按索引拼接检查合并代码确认按“文件索引分片大小”动态拼接相同文件第二次上传仍然慢秒传校验逻辑没有生效确认登记接口是否返回了缺失分片列表以及前端是否跳过已存在分片5.2 弱网和断网情况下的细节优化弱网断点续传里最怕的不是“断”而是“假死”。有一种情况特别坑链路质量差时HTTP请求会一直处于pending状态既没有成功也没有失败前端等不到回调就会一直挂着用户看到的就是“卡住了”。我针对这个问题加了超时控制$.ajax({ url: this.config.serverUrl.chunkUpload, timeout: 120000, // 单个分片最大等待时间 error: function(xhr, status) { if (status timeout) { // 手动把该分片置为失败触发重试机制 } } });超时时间设置需要考虑分片大小和链路带宽的匹配。比如8MB分片在10Mbps的链路上理论传输需要约6.5秒在100Mbps链路上不到1秒但加上排队和丢包重传2分钟的等待上限是合理的。如果分片调到16MB超时上限应同步提高到3分钟。另一个优化点是“并行分片和MD5预计算”当一个分片正在上传时后台线程预计算下一个分片的MD5。这样流水线作业能显著减少整体耗时。实测下来预计算可以让总耗时减少15%到20%因为在等待网络响应的空档里CPU是不闲着的。5.3 性能评估这套改造的收益用真实的18GB卫星视频文件做了一次完整测试测试环境内网千兆局域网、RTT 20ms、丢包率0.1%的工业交换网络客户端是国产台式机四核CPU、8GB内存。对比原生WebUploader和改造后的UploaderPro指标原生WebUploaderUploaderPro首次上传成功耗时失败中途断连7分12秒断网恢复后续传耗时需从头再传1分48秒同一文件重复上传无法识别完整传一遍秒传2秒内完成内存峰值1.6GB780MB分片请求数量单次完整上传约2304个2304个但失败重传仅2个最大的收益就是把一个“用户手动盯着传、断了一次就绝望”的场景变成了“扔在那里不用管、断网自动恢复继续”的场景。对现场运维人员来说这种体验提升是革命性的。5.4 国产化环境适配经验最后单独说国产化环境。军工现场很多用的是麒麟操作系统和国产浏览器这些浏览器内核大多是Chromium的特定版本有的比Chrome 69还老。我在适配中发现三个高频问题一是字体渲染和UI组件兼容性WebUploader自带的UI在国产浏览器上会有样式错乱我干脆弃用了它的自带面板只用它的js API界面完全用自己的React组件渲染彻底绕开样式兼容问题。二是文件路径问题在Linux桌面环境下File.name可能包含奇怪的编码服务端落盘时要统一做字符集转换否则合并文件名会乱码。三是软链接和挂载目录问题上传临时目录和最终归档目录可能在不同磁盘分区合并时如果跨文件系统移动大文件会非常慢。我建议服务端把临时目录和归档目录规划在同一文件系统内合并时用rename立即完成而不是复制。6. 从插件到平台一些后续可以继续做的事项目上线之后我在复盘时发现几个可以继续扩展的方向。第一个是把上传插件的状态数据接到运维监控平台通过心跳上报每个站点的上传成功率、平均速度、失败分片分布之类的指标。这样总部能提前预判哪些站点网络链路有问题而不是等现场人员打电话才排查。第二个方向是做一个上传策略的智能调度。比如在带宽紧张的时候自动降低并发数和分片大小把资源让给更高优先级的任务在夜间带宽空闲时自动提高并发数加速大批量历史数据回传。第三个方向是结合轻量级任务编排上传完成后自动触发后续的格式转换、视频抽帧、元数据提取步骤。这样“上传”就不只是文件传输动作而是整个数据入湖流程的起点。这些扩展方向我不打算一股脑写进插件里而是保持插件核心稳定把新能力做成独立的外围模块通过配置热插拔。毕竟军工系统的上线节奏不像互联网产品那么快每一步改动都要对稳定性和安全性负责。插件稳定、易排查、可演进比功能堆砌重要得多。从我个人经验看WebUploader虽然已经不是一个“新”项目了但它的架构设计底子非常好只要理解清楚它的分片事件机制和队列模型完全可以在它之上长出一个满足军工行业严苛要求的上传系统。这个改造项目最有价值的地方不是那几行代码而是整套“可断、可续、可校验、可恢复、可适配”的工程思路。希望这篇拆解能帮你少走一些弯路尤其是那些网络环境差、浏览器版本旧、文件动辄几十GB的场景值得把一个上传体验认认真真做到位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价