基于WebUploader改造的大文件分片断点续传插件设计与实现
2026/9/16 6:18:22 网站建设 项目流程

我们搞卫星视频回传这类业务的时候,最头疼的往往不是视频怎么处理,而是那动辄几个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,都是不对的。

分片大小要考虑三个约束:

  1. 内存占用。WebUploader 切片时会把分片数据读进内存,片太大内存吃紧,片太小数量太多,HTTP 请求次数爆炸。
  2. 网络环境。内网带宽高,片大点没毛病;公网弱网环境,片太大导致单片传输时间过长,失败重传成本高。
  3. 服务端接收能力。分片最终要在服务端合并,片数太多合并时文件描述符压力大。

我这边最终选了 4MB 一片。2GB 的视频切出来 512 片,不多不少。内网实测单片上传时间约1-2秒,弱网环境也能控制在30秒内,失败重传的代价可以接受。

2.2 并发数为什么是 3

WebUploader 的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?md5=xxx&chunkSize=4MB

返回已经收到的分片索引数组,例如[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(安装Flash)Flash支持分片、并发,但依赖Flash插件
IE6-IE8(无Flash)HTML4退化为普通表单上传,不做分片

在实际开发中,我直接把 HTML4 模式禁用了,因为退化到普通表单上传意味着超大文件直接失败,还不如明确提示用户换浏览器。配置方式:

// 注意 runtimeOrder 的写法:html5 优先,flash 兜底 runtimeOrder: 'html5 flash'

4.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-OriginAccess-Control-Allow-HeadersAccess-Control-Allow-Methods

另外注意withCredentials参数。如果上传接口需要校验登录态 Cookie,必须设置withCredentials: true,且服务端的Access-Control-Allow-Origin不能是通配符*,要明确写死域名。这个坑排查起来特别隐蔽,表现为“所有请求都返回 401 未登录”。

5. 实战:插件封装与核心代码

5.1 插件整体结构

我封装出来的插件分三层:最外层是业务 API,中间是 WebUploader 生命周期封装,最底层是断点续传状态管理。业务 API 层对外暴露uploadpauseresumecancel这几个方法就行,其他全屏蔽。

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 Conditioner(macOS 自带)或 Clumsy(Windows 工具),手动设置丢包率和延迟。测试链路是这样的:

  • 模拟 2% 丢包 + 200ms 延迟的弱网环境,跑 2GB 文件的完整上传
  • 每传 30% 强制刷新页面,验证断点恢复
  • 在分片请求进行中手动断网,过 30 秒恢复,验证失败重传和进度一致性
  • 用不同品牌浏览器交叉测试,确认跨浏览器表现一致

这套测试跑下来,插件才算真正达到了生产可用标准。

6.6 补充一个 MD5 计算的坑

分片上传本来是不需要算整个文件 MD5 的,但为了断点续传我们必须算。这个计算在浏览器里执行,2GB 文件算 MD5 需要几十秒到几分钟不等,工控机配置差点的会更久。所以我把 MD5 计算做成了异步任务,在用户选择文件后立即启动,计算期间用户可以做别的,计算完成后再自动开始上传。千万不要算 MD5 时把页面卡死,业务人员会以为系统崩了。

7. 最终效果与一些体会

做完这个插件后,我们单文件上传统计数据大概是这样的:1GB 文件在 10Mbps 带宽下 15 分钟左右传完,期间断网两三次也能正常续传;4GB 文件在弱网环境下原来要传一晚上还可能失败,现在基本半小时左右能完成,只有断网时间超过半小时才需要人工干预。

我个人的体会是,WebUploader 这个大龄开源库虽然官方已经归档了,但它解决的核心问题——浏览器端大文件分片上传——至今没有更好的替代方案。如果不依赖商业服务,自己从零手写分片、并发控制、队列管理这一整套,没有一到两个月下不来,而基于 WebUploader 改造,两周就能稳定跑起来。它的不足在于分片状态管理、断点续传这些高级能力需要自己补,但这恰恰是最有技术含量、最值得投入精力打磨的部分。

最后分享一个我们后期持续投入的方向,就是把这个插件继续优化成支持多语言、多数据源接入的通用版本——除了卫星视频,还接入了地图瓦片包、日志压缩包等场景,原理一模一样,换个文件 MIME 类型和上传接口就能用。如果你们也有类似的对超大文件上传稳定性要求高的业务场景,这套方案可以直接参考,少走不少弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询