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

资讯详情

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

基于WebUploader改造的大文件分片断点续传插件设计与实现

基于WebUploader改造的大文件分片断点续传插件设计与实现 我们搞卫星视频回传这类业务的时候最头疼的往往不是视频怎么处理而是那动辄几个GB的大文件怎么稳定传到服务器。卫星视频、遥感影像、现场无人机采集的高清视频单文件从500MB到10GB都很常见网络环境又不是机房那种专线偶尔还会断一下普通的上传组件根本扛不住。后来我把 WebUploader 拿出来改造了一版做成了跨浏览器的分片断点续传插件在这里把整个设计和实现过程整理出来供遇到类似场景的朋友参考。先说结论WebUploader 这玩意儿虽然年代久远、官方早就不更新了但它的核心设计放到今天依然能打。HTML5 File API 分片 并发控制这套机制配合我们自己封装的断点续传逻辑完全能撑起企业级的大文件上传需求。下面从选型、机制、实现到排查一步步拆开讲。1. 为什么选 WebUploader 来做底层1.1 场景痛点先摆出来我在做这个项目之前已经用原生 XMLHttpRequest 手写过一次分片上传踩了一堆坑。卫星视频上传这个场景有几个特点第一是文件大。单文件好几个GB一个完整的文件一次性读到内存里基本不可能必须切片。第二是网络环境复杂。业务人员可能在中转站、野外驻点也可能在办公内网网络质量参差不齐一传就是一两个小时中间断一次网就要重来这体验太恐怖。第三是浏览器环境不统一。现场那帮电脑上有老掉牙的 IE有国产双核浏览器有 Chrome你不能逼着用户统一换浏览器。这三个痛点叠加下来普通的上传方案基本废了——form 表单提交死路一条flash 上传控件已经淘汰原生 XMLHttpRequest 手写需要处理太多边界情况。1.2 市面方案对比当时摆在我面前的选择有这么几个方案分片断点续传并发控制旧浏览器兼容维护成本原生 XMLHttpRequest 手写可以全要自己写全要自己写全要自己写自己兼容极低但开发量爆表WebUploader内置半内置需要二次开发内置 threads 参数HTML5/Flash/HTML4 三级回退较低Plupload内置需要二次开发内置同上有回退较低商业云OSS SDK完善完善完善依赖浏览器版本高且数据要上云对比下来WebUploader 和 Plupload 其实同源都是百度和 Plupload 团队那个时代的产物。之所以最后选了 WebUploader主要是看中三点一是中文文档和社区资料多团队里其他人上手快二是 API 设计比 Plupload 更贴合国内开发者的使用习惯三是它的队列管理、事件机制、进度回调做得比较完整基于它做二次开发省不少事。1.3 插件化改造思路我不打算把 WebUploader 的调用代码散落在业务页面里而是封装成独立的插件。对外暴露统一的配置项和事件回调业务侧只需要传入文件、拿到进度和结果完全不需要关心底层分片、并发、续传这些细节。这样后续如果要把底层换成别的方案业务代码一行都不用动。插件的对外接口设计大致是这样的// 初始化上传插件 const uploader createVideoUploader({ url: /api/video/upload, chunkSize: 4 * 1024 * 1024, // 4MB一片 threads: 3, // 同时3个分片并发 maxSize: 20 * 1024 * 1024 * 1024, // 最大20GB // ... 其他配置 }); // 监听事件 uploader.on(uploadProgress, (percent, file) {}); uploader.on(uploadComplete, (fileInfo) {}); uploader.on(uploadError, (err) {});2. 核心机制拆解分片、并发与队列2.1 分片大小到底怎么定这是第一个要拍板的技术参数。分片大小直接影响上传效率和断点续传的粒度。我见过有人随便填个 1MB也有人填 100MB都是不对的。分片大小要考虑三个约束内存占用。WebUploader 切片时会把分片数据读进内存片太大内存吃紧片太小数量太多HTTP 请求次数爆炸。网络环境。内网带宽高片大点没毛病公网弱网环境片太大导致单片传输时间过长失败重传成本高。服务端接收能力。分片最终要在服务端合并片数太多合并时文件描述符压力大。我这边最终选了 4MB 一片。2GB 的视频切出来 512 片不多不少。内网实测单片上传时间约1-2秒弱网环境也能控制在30秒内失败重传的代价可以接受。2.2 并发数为什么是 3WebUploader 的threads参数控制同时上传的分片数量。我特意测过不同并发数的表现并发数带宽利用率服务器压力弱网表现1低有等待间隙最小稳但慢3高且平稳可接受稳速度好5很高可跑满开始有压力偶尔超时10能跑满蹭蹭涨极易超时3 这个数是综合 WebUploader 社区经验和我自己的压测结果得出的。它能在带宽利用和服务器稳定性之间取得平衡。而且要注意并发数是“每个文件”的并发如果用户同时拖进来三个文件WebUploader 是按队列依次处理的不会三个文件一起抢带宽这个下面说。2.3 队列管理与文件校验WebUploader 里有个很实用的机制文件先加入队列校验通过后才开始上传。这个机制被我用来做上传前的预处理。校验项包括文件扩展名、文件大小、文件是否重复。卫星视频的格式一般是 MP4、MOV、TS 这类在accept配置里限制住。文件大小上限也在这里拦一道防止运维误拖进来一个 50GB 的 RAW 格式素材把磁盘塞爆。重复检测也是 WebUploader 自带的同一个文件拖两遍会直接提示“该文件已添加”省得用户自己犯迷糊。3. 断点续传从“半内置”到真正可用3.1 WebUploader 原生的断点续传实际上不够用说实话WebUploader 原生的断点续传只是个空壳它是通过chunked和服务器分片存储来实现的但缺少一个核心能力让服务端知道“我这个文件已经传了哪些分片”。浏览器一刷新内存里的状态就丢了所有分片重新传一遍等于没断点续传。我们做断点续传核心逻辑就一句话上传前先向服务端查询该文件的完成情况只上传缺失的分片。要实现这个就要给每个文件算一个唯一标识。我用了“文件大小 文件MD5 ”的组合。前端用 WebUploader 自带的 MD5 计算能力在后台上默默算对它内置了 MD5不用自己引库。3.2 前端断点续传状态管理刷新后怎么恢复这个方案我落地后效果很好分享下设计。前端在开始上传时生成一个上传任务记录包含文件唯一标识md5、文件名、大小、分片大小、分片总数、文件在队列里的序号然后存入 localStorage。上传过程中每个分片成功后把已完成的索引号追加到记录里。刷新页面后前端重新初始化时先扫一遍 localStorage发现有未完成的上传任务就把对应文件直接恢复到队列里然后调用服务端查询接口把服务端已经收到的分片索引拉回来跳过这些分片只传缺失的。这里有个细节容易踩坑localStorage 存的是任务记录不是文件内容。刷新后浏览器里的 File 对象已经没了必须让用户重新选择一次文件。但是没关系因为文件 MD5 相同服务端认的是 MD5 和分片索引用户重新选文件之后前端算一遍 MD5发现和本地记录一致就直接恢复续传了。3.3 服务端三个接口的设计断点续传要跑通服务端得配合三个接口查询接口GET /api/video/upload/status?md5xxxchunkSize4MB返回已经收到的分片索引数组例如[0, 1, 3, 5]。前端拿到之后把 2、4、6 这些缺失的分片补传上去。上传分片接口POST /api/video/upload/chunk参数包括 md5、chunkIndex、ChunkSize、文件分片内容。服务端把分片暂存在临时目录命名规则是{md5}_{chunkIndex}.part。合并接口POST /api/video/upload/merge参数包括 md5、原始文件名、总分片数。服务端检查所有分片齐全后按索引顺序拼接成完整文件。3.4 秒传的实现既然 MD5 已经算了秒传就是顺手的事。服务端在合并后把完整文件的 MD5 存入文件索引表。下次有人上传相同文件时查询接口直接返回“该文件已存在、无需上传”的状态前端就秒跳过。卫星视频这种素材经常会有重传需求比如同一个区域的不同时间段的视频大小完全一致但内容不同MD5 不同不会碰撞。但如果真遇到内容完全相同的文件秒传能省一个多小时。4. 跨浏览器兼容的设计与实现4.1 runtime 选择策略WebUploader 最让我佩服的就是它的 runtime 分层设计。它会按优先级自动选择 HTML5 → Flash → HTML4 三种运行模式浏览器环境使用 runtime能力Chrome/Edge/Firefox 现代浏览器HTML5完整支持分片、并发、进度IE8-IE10安装FlashFlash支持分片、并发但依赖Flash插件IE6-IE8无FlashHTML4退化为普通表单上传不做分片在实际开发中我直接把 HTML4 模式禁用了因为退化到普通表单上传意味着超大文件直接失败还不如明确提示用户换浏览器。配置方式// 注意 runtimeOrder 的写法html5 优先flash 兜底 runtimeOrder: html5 flash4.2 那些年我们踩过的浏览器兼容坑第一坑IE 的单文件大小限制。IE 的 XMLHttpRequest 在某些版本有怪癖直接加载大文件到内存会崩。WebUploader 的 HTML5 分片其实绕开了这个坑但 IE 下内存回收不及时传完几个大文件页面会明显变卡。解决办法是传完一个文件调用一次uploader.removeFile(file)释放引用。第二坑国产双核浏览器。很多电脑上装的是 360、QQ 浏览器之类的双核浏览器默认走兼容模式IE内核根本不支持 HTML5 分片。我的处理姿势是加一个前置检测如果发现浏览器模式是 IE 或者不是标准模式弹窗提示用户手动切换到“极速模式”Chrome内核。这个提示得写得通俗点现场业务人员不懂技术我直接写“为了正常上传视频请点击地址栏右侧的闪电图标切换到极速模式”。第三坑文件名编码。卫星视频的文件名经常带中文、空格、括号如果服务端没做解码文件名会直接乱码甚至截断。在 formData 里传文件名时保险起见用encodeURIComponent编码服务端再解码。还有文件名里的点号file.name在部分浏览器里可能只保留最后一个点后面的部分这类问题最好前后端各做一层basename处理。4.3 CORS 与 Cookie 处理如果前端页面和上传接口不在同一个域名经常的事前端在静态服务器上传接口在业务服务器跨域就来了。WebUploader 内部发的是 XMLHttpRequest跨域时需要在服务端配置好 CORS 头Access-Control-Allow-Origin、Access-Control-Allow-Headers、Access-Control-Allow-Methods。另外注意withCredentials参数。如果上传接口需要校验登录态 Cookie必须设置withCredentials: true且服务端的Access-Control-Allow-Origin不能是通配符*要明确写死域名。这个坑排查起来特别隐蔽表现为“所有请求都返回 401 未登录”。5. 实战插件封装与核心代码5.1 插件整体结构我封装出来的插件分三层最外层是业务 API中间是 WebUploader 生命周期封装最底层是断点续传状态管理。业务 API 层对外暴露upload、pause、resume、cancel这几个方法就行其他全屏蔽。5.2 初始化代码const uploader WebUploader.Uploader({ // 上传接口地址 server: /api/video/upload/chunk, // 选择文件的按钮 pick: { id: #picker, multiple: false }, // 分片相关配置 chunked: true, // 开启分片 chunkSize: 4 * 1024 * 1024, // 4MB threads: 3, // 并发数 // 其他配置 fileVal: file, fileNumLimit: 5, fileSizeLimit: 20 * 1024 * 1024 * 1024, // 20GB accept: { title: Video, extensions: mp4,mov,ts,avi,flv, mimeTypes: video/* }, // 自动上传改为手动方便先算md5 auto: false, // 这个key很重要服务端靠它区分是分片请求还是正常上传 formData: { type: chunk }, runtimeOrder: html5 flash });5.3 断点续传状态记录核心代码这是实现断点续传最核心的一段。每当一个分片上传成功后把分片索引写进本地记录uploader.on(uploadSuccess, function(file, response) { const record getRecordByFile(file); if (!record) return; // response 里返回当前分片的索引这是后端在返回体里带上的 const chunkIndex response.chunkIndex; if (!record.finishedChunks.includes(chunkIndex)) { record.finishedChunks.push(chunkIndex); saveRecord(record); } // 记录进度百分比 const percent Math.round(record.finishedChunks.length / record.totalChunks * 100); updateProgressUI(percent); });注意这里有个关键点owedChunkIndex是 WebUploader 内部的当前分片索引属性不同版本名字可能不一样而且官方 API 文档里没写是从源码里翻出来的。所以从 AE 上传到服务端返回的response里带回chunkIndex更稳妥不依赖内部属性。5.4 恢复续传逻辑页面刷新后用户重新选择同一个文件插件计算完 MD5 后识别出这是“未完成任务”于是调查询接口async function checkResume(md5, chunkSize, fileName, fileSize) { const res await fetch(/api/video/upload/status?md5${md5}chunkSize${chunkSize}); const data await res.json(); if (data.status completed) { // 秒传直接通知成功 return { resume: false, completed: true }; } if (data.status partially_done) { // 有已完成的分片跳过它们 uploader.option(formData, { type: chunk, md5: md5, fileName: encodeURIComponent(fileName), finishedChunks: JSON.stringify(data.finishedChunks) }); return { resume: true, completed: false, finishedChunks: data.finishedChunks }; } // 全新上传 return { resume: false, completed: false }; }拿到服务端返回的finishedChunks后前端需要把每个分片的状态同步给 WebUploader让它跳过已经上传过的分片。这个操作 WebUploader 原生不提供得遍历所有分片把已完成的分片标记成“已传”状态。我当时的做法是记录下服务端返回的已完成索引在上传时钩子uploadBeforeSend里拦截uploader.on(uploadBeforeSend, function(file, data, header) { const record getRecordByFile(file); // 如果当前分片已经传过直接终止这个分片的请求 if (record record.finishedChunks.includes(data.chunk)) { return false; // 返回false跳过该分片 } });这个拦截方法实测有效但也有个副作用跳过的分片不会触发uploadSuccess事件所以进度计算不能依赖事件驱动要自己根据总片数 - 已完成片数来推算。这个细节很坑我第一次实现时进度卡在 30% 不动排查了半天发现是事件没触发。5.5 暂停与取消WebUploader 原生提供了stop()方法暂停上传removeFile()取消文件。stop()之后整个队列中止正在传输的请求终止。我封装的resume()就是重新调用uploader.upload()继续队列结合上面的uploadBeforeSend跳过已完成分片就实现了暂停之后的续传。5.6 后端合并时的要注意的点服务端合并分片时我建议不要直接用fs.appendFile循环写文件。正确做法是先用文件锁或者临时目录机制把分片校验完整后再合并然后检查合并后的文件大小是否和前端报过来的一致。大型文件超过2GB在 32 位系统上会有超过 2GB 文件限制的问题服务端必须保证 64 位系统和相应文件系统支持。另外合并时注意释放分片文件句柄否则删除临时分片时在 Windows 上会报“文件被占用”。6. 常见问题与排查技巧实录6.1 分片上传完成后合并失败合并失败最常见的原因是分片缺失或分片重复。排查方法是先核对服务端收到分片的耗时记录确认前端上报的总分片数和服务端的实际分片数一致。如果分片数对合并还是失败很可能是并发上传时同一个文件的分片在服务端被多个回调同时写入导致文件错乱。解决办法是服务端在合并阶段加锁或者用临时文件名 写完成后再改名的方式规避并发写入冲突。6.2 页面刷新后恢复不了进度这个现象十有八九是 localStorage 里的任务记录和服务端的分片状态对不上。最常见的原因用户刷新前已经上传了 100 个分片但 localStorage 的记录只更新到 80 个。因为uploadSuccess事件和 localStorage 写入之间有个时间差如果浏览器在写入前崩溃记录就丢了。我的解决办法是双写每次分片成功先写 localStorage再更新 UI。即使中间崩溃最多丢一个分片的记录服务端查询接口会找回真实状态。6.3 并发数过高导致服务器连接数打满测试环境一切正常一上生产就发现服务器 Nginx 报“too many open files”。原因是并发 3 个分片上传如果每个分片请求还需要额外查询接口、静态资源请求连接数一下子就上去了。解决办法是把threads调小或者在全链路加上连接数监控。卫星视频上传的时间窗口往往集中在特定时段运维那边最好提前调大 Nginx 的worker_connections。6.4 超大文件上传时浏览器卡死我遇到过 10GB 文件上传到一半浏览器无响应的情况。定位下来是 WebUploader 在分片时把所有分片一次性解析到内存导致内存暴涨。解决办法有两个一是控制最大允许上传的文件大小比如限制到 5GB超过就提示用户用专门的桌面端上传工具二是给浏览器提个醒上传前先做一次内存检测如果剩余内存不足提示用户关闭其他标签页再传。6.5 稳定性测试方案上线前我做了三天的稳定性测试这里分享一个环境配置。弱网模拟用 Network Link ConditionermacOS 自带或 ClumsyWindows 工具手动设置丢包率和延迟。测试链路是这样的模拟 2% 丢包 200ms 延迟的弱网环境跑 2GB 文件的完整上传每传 30% 强制刷新页面验证断点恢复在分片请求进行中手动断网过 30 秒恢复验证失败重传和进度一致性用不同品牌浏览器交叉测试确认跨浏览器表现一致这套测试跑下来插件才算真正达到了生产可用标准。6.6 补充一个 MD5 计算的坑分片上传本来是不需要算整个文件 MD5 的但为了断点续传我们必须算。这个计算在浏览器里执行2GB 文件算 MD5 需要几十秒到几分钟不等工控机配置差点的会更久。所以我把 MD5 计算做成了异步任务在用户选择文件后立即启动计算期间用户可以做别的计算完成后再自动开始上传。千万不要算 MD5 时把页面卡死业务人员会以为系统崩了。7. 最终效果与一些体会做完这个插件后我们单文件上传统计数据大概是这样的1GB 文件在 10Mbps 带宽下 15 分钟左右传完期间断网两三次也能正常续传4GB 文件在弱网环境下原来要传一晚上还可能失败现在基本半小时左右能完成只有断网时间超过半小时才需要人工干预。我个人的体会是WebUploader 这个大龄开源库虽然官方已经归档了但它解决的核心问题——浏览器端大文件分片上传——至今没有更好的替代方案。如果不依赖商业服务自己从零手写分片、并发控制、队列管理这一整套没有一到两个月下不来而基于 WebUploader 改造两周就能稳定跑起来。它的不足在于分片状态管理、断点续传这些高级能力需要自己补但这恰恰是最有技术含量、最值得投入精力打磨的部分。最后分享一个我们后期持续投入的方向就是把这个插件继续优化成支持多语言、多数据源接入的通用版本——除了卫星视频还接入了地图瓦片包、日志压缩包等场景原理一模一样换个文件 MIME 类型和上传接口就能用。如果你们也有类似的对超大文件上传稳定性要求高的业务场景这套方案可以直接参考少走不少弯路。
返回列表