上周刚完成单位内部系统一个上传模块的改造验收,需求本身不复杂:给定一批几个GB级别的卫星视频文件,要能在单位内部网络里跨浏览器传到服务器并完成归档,过程中最怕的就是断网、关机、浏览器闪退以后全部重来。市面现成的组件不少,筛了一圈,最后回到WebUploader——这个项目很老了,2016年前后就基本处于半停更状态,可它在分片、队列、UI交互上的底子确实扎实,对国内政企内网那套老环境适配也最全面。问题在于原版默认走Flash通道,放在今天的浏览器环境下根本跑不起来,更别说断点续传、超大文件内存控制这些硬需求。
如果你也在做类似的事:面对几百MB甚至好几个GB的卫星视频、执法记录仪录像、归档文件,想找一个能支撑跨浏览器、可断点续传、能落地的超大附件上传方案,这篇改造记录应该能帮上忙。我会把整个改造过程摊开讲,从选型判断、Flash依赖摘除、分片续传链路设计,到军工内网场景下容易踩的内存、校验、兼容性坑,尽量都说清楚。
1. 为什么不直接用现成组件,而要动WebUploader的刀子
1.1 市面方案筛了一遍之后,还是它的底子最合适
我接手这个需求时,第一反应也不是改造WebUploader,而是先看了一圈别的方案。
先看了Resumable.js,分片续传逻辑写得确实漂亮,gzip后也就十几KB,但它只解决上传本身,队列管理、UI、进度统计全都要自己拼。FineUploader功能全,可对国内老浏览器的兼容性不太友好,而且它的续传是“服务端记住已经收到哪些分片”,需要后端接口按它的语义来,定制成本不低。Uppy那个现代化的架构在互联网项目里很香,但在一个不能随便拉第三方依赖、终端环境五花八门的内部系统里,光是把它的各种插件装齐就够折腾。
最后绕回来再看WebUploader,它的优势就体现出来了:队列、并发控制、分片策略、MD5计算、UI组件这一整套都是现成的,而且对IE系老浏览器做了大量兼容兜底。说白了,我需要解决的不是“从零写一个上传组件”,而是“把一个架构思路成熟但传输通道落后的组件改造成现代浏览器可用”。在这个前提下,WebUploader的改造性价比反而是最高的。
当然,直接拿官方原版用是不行的,必须动刀子。原版有几个硬伤在卫星视频这种超大文件场景下会直接劝退。
1.2 原版在超大视频文件场景下的三个硬伤
第一个硬伤是Flash依赖。WebUploader出生那个年代,HTML5还是后起之秀,它默认用Flash作为主力上传通道,HTML5只是“顺便支持”。现在Chrome、Edge、Firefox早就不支持Flash插件了,内网终端也不可能为了一个上传功能去折腾插件白名单。不把Flash依赖摘掉,组件在新浏览器里就是个摆设。
第二个硬伤是内存控制。原版在做分片上传时,会把每个分片都当成一个独立文件对象放进队列,遇到大文件的时候还会出现一次性把大量分片读进内存的情况。几十MB的文件无所谓,一旦到了GB级,浏览器内存占用直接飙升,最后页面越来越卡,严重的直接崩溃。卫星视频动辄几个GB,这种内存策略是致命的。
第三个硬伤是“假断点续传”。官方文档里说的分片失败重传,只是在同一个页面会话内部有效——你刷新了一下页面,或者浏览器崩溃重启,之前的进度就全没了。真正的断点续传至少要解决两件事:第一,文件有一个稳定的唯一标识,重新选择同一个文件能识别出来;第二,服务端能告诉你“哪些分片我已经有了,你不用再传”。这两点原版都没做。
除了这三点,原版还存在文件合并时缺少完整性校验、并发数不适用于大文件、进度统计口径单一等问题。所以整体思路就是:保留WebUploader的队列管理、分片策略、交互UI这套骨架,把上传内核换成HTML5实现,再在它上面补齐断点续传、秒传和完整性校验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆掉Flash底座:上传内核的HTML5改造
2.1 浏览器能力盘点与三级降级设计
在动手写代码之前,我先把目标终端跑一遍能力检测。核心就三样:是否支持File API,是否支持Blob.slice,是否支持XMLHttpRequest Level 2。这三样是HTML5分片上传的地基。
WebUploader自带的Support模块能检测一部分能力,但它对HTML5的支持判断比较粗糙,我直接用它暴露的Support.html5开关作为一级判断,再额外补一个canSlice的检测:
javascript复制// 补充能力检测:在高版本浏览器里 Blob.slice 是标准写法,
// 但在老项目里经常碰到只支持 mozSlice / webkitSlice 的情况
function detectSliceSupport() {
const blob = new Blob(['a', 'b', 'c']);
if (blob.slice) return 'slice';
if (blob.mozSlice) return 'mozSlice';
if (blob.webkitSlice) return 'webkitSlice';
return null;
}
整体降级顺序我是这样设计的:
- 浏览器支持HTML5分片上传:走新写的HTML5通道,支持断点续传、并发控制,这是主路径。
- 浏览器不支持但允许Flash加载(极少数内网老环境):保留WebUploader原来的Flash通道,但明确定位为“兼容可用”,不做续传承诺。
- 浏览器连分片都不支持:退化为普通表单文件上传,并且在前端明确提示“当前浏览器不支持大文件断点续传,建议使用Chrome或Edge”。
这个三级策略一定要在一开始就确定,不然后期接入各种老浏览器时会被零散需求拖死。
2.2 用Blob.slice切分文件,用XMLHttpRequest顶替Flash通道
WebUploader的分片逻辑本身是现成的,它会在文件对象上生成file.blocks数组,每个block里有start和end字节位置。原版的Flash通道会把每个block包装成Runtime里的一个File对象再通过Flash Socket发送。我的改造方案很简单:拿到block之后不再交还给Flash,而是直接用Blob.slice从源文件上切出这一段二进制,然后用XMLHttpRequest发送。
核心发送逻辑长这样:
javascript复制function uploadChunk(file, block, options) {
return new Promise((resolve, reject) => {
const chunkBlob = file.source.slice(block.start, block.end);
const formData = new FormData();
formData.append('fileName', encodeURIComponent(file.name));
formData.append('fileId', file.uniqueId);
formData.append('chunkIndex', block.index);
formData.append('totalChunks', file.blocks.length);
formData.append('chunk', chunkBlob, file.name + '.part' + block.index);
const xhr = new XMLHttpRequest();
xhr.open('POST', options.uploadUrl, true);
xhr.timeout = options.timeout || 300000;
xhr.upload.onprogress = (evt) => {
if (evt.lengthComputable) {
// 这里把单分片进度回调给上层,统一进队列视图
block.loaded = evt.loaded;
options.onProgress && options.onProgress(file, block, evt);
}
};
xhr.onload = () => {
const res = parseResponse(xhr.responseText);
if (xhr.status >= 200 && xhr.status < 300 && res && res.code === 0) {
block.uploaded = true;
resolve(res);
} else {
reject(new Error(res && res.msg || '分片上传失败'));
}
};
xhr.onerror = () => reject(new Error('网络异常'));
xhr.ontimeout = () => reject(new Error('上传超时'));
xhr.send(formData);
});
}
有几个细节值得单独说一下。第一个是分片命名,我建议前端不要把文件名直接拼接进分片序号。因为某些网盘同步工具或者老版本浏览器会篡改文件名里的特殊字符,服务端合并时一排序就全乱了。传参数的时候把文件名用encodeURIComponent包一下,服务端存HttpServletRequest参数时再解码一次,能避免很多乱码问题。
第二个是超时。卫星视频文件传输链路通常很长,单个分片在弱网下可能慢得离谱,所以超时时间我调到了300秒,而不是默认的30秒。这个参数不要写死,要能通过配置项传进去,后续根据网络环境调整。
第三个是错误处理。分片上传遇到网络抖动失败是常态,但不要一失败就把整个文件标记成失败。分片失败只影响当前分片,在重试机制里单独处理就行,后面第三章会细说。
2.3 事件驱动重构:让上传控件保持“可插拔”
WebUploader原版的Runtime机制是插件式的,我改造时也沿用了这个思路。新写的HTML5通道对外暴露的接口和原版保持基本一致:addFile、upload、retry、stop、destroy。这样对外暴露的API不变化,上层业务代码改动面就小很多。
这里有一点值得留意:WebUploader原版内部用的是事件发布订阅,所有上传状态变化都会抛事件,比如startUpload、uploadProgress、uploadComplete。我在重构内核时保留了这套事件机制,同时新增了几个关键事件:
javascript复制this.owner.trigger('chunkUploaded', file, block); // 单个分片成功
this.owner.trigger('chunkError', file, block, err); // 单个分片失败
this.owner.trigger('hashCalculated', file, md5); // 文件指纹计算完成
this.owner.trigger('resumeFromCheckpoint', file, uploadedChunks); // 续传命中
新增事件的好处是,当你后续做上报、日志、用户提示的时候,不需要去修改上传内核,直接订阅这些事件就行。比如我在改造后期接到一个需求:要统计每个文件“真正从网卡上传的数据量”和“通过续传省掉的数据量”,就是靠监听chunkUploaded和resumeFromCheckpoint之间的差值算出来的。
3. 分片、断点续传、秒传一条龙实现
3.1 文件唯一指纹:SparkMD5增量计算
分片续传的前提,是同一个文件不管在什么时间、什么设备上再次选择,都能被识别成“同一个文件”。最常见的方案是计算整个文件的MD5。等这个文件所有分片都传完了,服务端也可以再算一次完整MD5,前后比对,这就是秒传和完整性校验的基础。
但这里有个很现实的坑:几个GB的文件,如果用最笨的办法一次性读进内存再算,浏览器直接崩。正确做法是增量计算,一次只读几MB进内存,喂给MD5算法,算完就丢,再读下一段。我用的是SparkMD5库,它支持增量模式。
javascript复制function calcFileMD5(file, chunkSize, onProgress) {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
let offset = 0;
reader.onerror = () => reject(reader.error);
reader.onload = (e) => {
spark.append(e.target.result);
offset += chunkSize;
if (offset < file.size) {
onProgress && onProgress(Math.min(100, offset / file.size * 100));
readNext();
} else {
resolve(spark.end());
}
};
function readNext() {
const slice = file.slice(offset, offset + chunkSize);
reader.readAsArrayBuffer(slice);
}
readNext();
});
}
在WebUploader改造里,我建议把MD5计算放到一个Web Worker里去做。为什么?因为哪怕已经是增量计算、一次只读2MB,主线程仍然要承担ArrayBuffer到字符串的转换、对象创建这些开销。文件一大,主线程卡顿非常明显,用户能直接看到页面冻结好几秒。放进Worker之后,主线程只负责接收进度消息,UI完全不会卡。
几GB的文件在普通办公电脑上算MD5大概要10到30秒,这个耗时一定要做成可见的。我是在文件名旁边显示一个“文件校验中 xx%”的独立进度条,等MD5算完,再无缝进入上传状态。
3.2 续传判定:让服务端告诉你哪些分片还没到
MD5计算完成后,文件就有了唯一标识file.uniqueId。接下来的操作是:拿着这个ID去问服务端“这个文件你手上已经有什么了”。
服务端要提供一个查询接口,语义大概是:根据fileId返回已经成功接收并存储的分片下标数组。
javascript复制async function fetchUploadedChunks(file) {
const res = await fetch(`/api/upload/check?fileId=${encodeURIComponent(file.uniqueId)}`);
const data = await res.json();
if (data.code === 0) {
return data.data.uploadedChunkIndexes || [];
}
return [];
}
拿到已传分片数组之后,前端把对应分片的状态直接标记为skipped,不进入待传队列。这里有一个小坑:已传分片的下标服务端是按数字存的,前端做比对时一定要用Number类型强转,不要用字符串比较,否则'9'和'10'的排序会出问题。
续传这里还衍生出一个秒传逻辑:如果服务端返回的已传分片数量等于总分片数,那说明这个文件已经完整上传过了,直接跳到合并接口拿文件地址就行。实际中我遇到过同一份卫星视频素材被不同科室重复上报的情况,秒传功能直接帮他们省掉了重复传输时间。
查询接口带宽很小,不会对服务器造成压力,但要注意加个简单缓存,避免同一个fileId短时间被大量请求打到后端。
3.3 并发控制与失败重试策略
分片都准备好了,接下来就是真正上传。原版WebUploader默认并发数是3,在大文件场景下这个配置偏激进。原因有两点:并发数越高,对客户端内存和网卡的瞬时压力越大;同时服务器端接收分片时还要做临时文件写入,并发太高容易IO抖动。
我最终选择的是“分片大小2MB,并发数2”。2MB的分片在弱网下重试成本可控,2路并发又能把带宽跑满又不至于压垮页面。当然这个参数不是死的,配置项里开放出来,实测下来带宽足够宽的内网用4MB分片效果也不错。
并发控制不需要引入复杂框架,WebUploader的队列本身就支持,我是在queue里做了一个计数信号量:
javascript复制class ChunkScheduler {
constructor(limit) {
this.limit = limit;
this.activeCount = 0;
this.pendingQueue = [];
}
push(task) {
return new Promise((resolve, reject) => {
this.pendingQueue.push({ task, resolve, reject });
this.pump();
});
}
pump() {
while (this.activeCount < this.limit && this.pendingQueue.length) {
const { task, resolve, reject } = this.pendingQueue.shift();
this.activeCount++;
task()
.then(resolve, reject)
.finally(() => {
this.activeCount--;
this.pump();
});
}
}
}
失败重试我做了3次,重试间隔采用指数退避:第一次失败等1秒,第二次等2秒,第三次等4秒,三次都失败就把这个分片标记为error,同时暂停整个文件的上传队列,弹提示让用户决定是重试还是放弃。不要自动无限重试,否则网络长时间不可用时,页面会陷入无意义的请求循环。
3.4 合并流程与最终完整性校验
所有分片传完之后,前端调用合并接口。这个接口做的事情是:按chunkIndex的顺序把所有临时分片拼成完整文件,写入最终归档目录。合并接口不需要把整个文件读进内存再写出来,流式读写就行,具体后端实现这里不多说,前端要注意的是一点:合并是异步的,对超大文件可能要几十秒甚至几分钟,不能用同步拉取的方式等待结果。
我这边合并接口的返回格式是:{ code: 0, data: { status: 'merging' | 'done', progress: 0-100, fileUrl: 'xxx' } }。前端拿到merging状态之后,要起一个轮询任务,定时调用合并进度查询接口,等状态变成done之后才算真正完成。
可靠性核心在于最后一道校验。合并完成后,服务端要计算完整文件的MD5,并和前端上传前算好的MD5比对。一致,返回成功;不一致,说明中间有分片损坏或者乱序,立刻触发“标记该文件为异常,允许用户一键重新上传缺失分片”的逻辑。这一步绝对不能省,尤其卫星视频这种关键数据,传一个坏文件上去比不传还麻烦。
4. 超大视频文件场景下容易被忽略的工程细节
4.1 内存监控与浏览器崩溃预防
这几个GB的文件一进来,最先扛不住的是浏览器内存。我改造过程中做过一次原型验证:用原版WebUploader传一个4.7GB的视频文件,分片大小默认4MB,结果传了不到30%页面就崩了。打开任务管理器看,Chrome的GPU进程和Renderer进程内存占用都超过了2GB。
换到新改造的HTML5通道之后,内存问题会好很多,因为分片不再全部进内存,每片发完即丢。但我在实际验证中还是发现了一个隐藏的上涨点:每次用FileReader读取分片时会生成一个ArrayBuffer,如果读完后不手动把引用关系清掉,即使分片已经发送完成,V8的垃圾回收也不会立刻把它回收,长时间传输下内存还是会慢慢涨。
解决办法是:每次上传完成分片后,把block.loaded、临时FormData引用置空,同时在requestIdleCallback或者低优先级任务里调用一次window.gc(只在DevTools实验环境下有效),更多的还是靠代码里主动释放引用。
另一个比较实用的手段是内置一个内存监控小部件:利用performance.memory.usedJSHeapSize定时采样,当JS堆内存超过某个阈值时,自动降低并发数甚至暂停队列等待GC。实测下来这个策略非常管用,它让组件在8GB内存的老办公电脑上也能稳定跑完7GB的文件传输。
4.2 断网、断电、标签页关闭后如何接续
断点续传能不能真正落地,关键看极端故障下能不能恢复。我总结了一下,至少要覆盖下面三类场景:
- 网络断开:当前正在传的分片会报错,重试也失败。这时暂停队列,启动网络状态监听,等online事件触发后自动恢复上传。
- 浏览器标签页被误关:上传进程直接消失。解决方式是把文件的关键状态写到localStorage,等用户重新打开页面并再次选择同一文件时,组件通过fileId查询服务端已传分片,接着传。
- 系统断电:localStorage其实是有丢失风险的,所以缓存的角色只是辅助。真正可靠的还是服务端的分片记录,前端只需要保证“重新选择文件之后还能算出一模一样的MD5”就行。
这里有个值得注意的设计:localStorage里存的状态不要存储整个分片表,那样数据量太大。我只存文件的基本信息和状态标记,比如{ fileId, fileName, fileSize, uploadTime, status: 'uploading' },实际进度以服务端check接口返回为准。这样即使localStorage丢失,也只不过是多调一次check接口而已。
4.3 进度、网速、剩余时间的统计算法
用户对大文件上传最敏感的体验,就是进度条是不是靠谱。原版WebUploader的进度是“已上传字节数/总字节数”,但这里有个问题:如果文件做过断点续传,前面跳过的分片不算新上传量,如果还拿“已上传字节/总分片字节”来算,进度会在续传命中后直接跳到50%甚至90%,用户会以为传完了,但实际上后面还有一堆分片在跑。
我的处理方式是分三个指标显示:
- “总进度”按文件角度算,已完成分片数/总分片数,跳过的不算已完成,但它本来就对应文件真实落盘进度,所以跳变是合理的,要展示成“服务端已接收”而不是“本次已上传”。
- “本次会话上传量”单独统计,从组件初始化之后真正通过网卡发出去的数据量,这个用来看网速和剩余时间。
- “剩余时间”用指数加权移动平均来估算,不要用瞬时速度直接算,否则网速一抖,时间显示忽上忽下,很影响信任感。
剩余时间估算我贴一下核心代码,非常简单但效果显著:
javascript复制let emaSpeed = 0;
const alpha = 0.3; // 平滑系数
function updateSpeed(instantSpeed) {
emaSpeed = emaSpeed === 0 ? instantSpeed : alpha * instantSpeed + (1 - alpha) * emaSpeed;
return emaSpeed;
}
function calcRemainTime(remainBytes, speed) {
if (!speed || remainBytes <= 0) return 0;
return Math.max(0, remainBytes / speed);
}
4.4 实测数据:不同网络环境下的表现
改造完成之后,我在内网做了几轮压测,挑了三个典型文件大小,记录数据如下:
| 文件大小 | 分片大小 | 并发数 | 正常上传耗时 | 平均上行速度 | 备注 |
|---|---|---|---|---|---|
| 1.2GB | 2MB | 2 | 约4分20秒 | 约4.7MB/s | 正常无抖动 |
| 3.6GB | 2MB | 2 | 约13分50秒 | 约4.5MB/s | 中间断网一次,恢复后续传完成 |
| 7.9GB | 4MB | 3 | 约26分 | 约5.2MB/s | 高带宽内网,内存峰值约680MB |
第三行是我后来把分片调成4MB、并发调到3之后的优化结果,可以看到总耗时下降明显。但要注意,这个配置是在高带宽内网环境下测的,如果访问链路是跨地域专线,还是建议回到2MB、并发2的保守配置,否则重试成本会上升。
测试过程中还发现一个容易被忽视的问题:文件太大时,MD5计算本身会占用十几秒甚至半分钟,如果用户在这期间点击了取消,前台MD5计算任务会继续在后台跑,白白消耗CPU。后来的处理是:在取消逻辑里加了一个abortMD5标志位,计算函数每读一个分片就检查一次,如果标志位为true,立即终止并resolve一个null值。
5. 跨浏览器兼容性测试与排坑记录
5.1 Chrome、Firefox、Edge差异处理
先说结论:在我实际测试的Chrome 80+、Firefox 78+、Edge 80+上,HTML5分片上传核心逻辑是可以完全通用的。但确实有几个细节点需要特别处理。
第一个是Chrome和Firefox对并发连接数的限制。Chrome对同一域名的并发HTTP连接上限是6,Firefox是6,Edge也是6。我这边开了2路上传,再加上MD5计算请求、check请求、合并轮询请求,同时也就3到4个连接,不会触发限制。但如果把并发数设到4甚至5,再加上其他业务请求,很容易碰到“连接被挂起”的问题,看起来像是代码bug,其实是浏览器级别的连接池占满了。所以我建议并发数不要超过3。
第二个是File对象和Blob对象的size属性兼容性。在Firefox老版本里,blob.slice返回的Blob对象可能没有name属性,所以FormData里要手动给chunk设置文件名。另外Firefox对xhr.responseType为arraybuffer的处理和Chrome略有差异,如果需要读取响应二进制,建议统一用responseText并让后端返回JSON字符串。
第三个是Edge旧版(EdgeHTML内核)不支持File.slice的第三参数和某些MIME处理。不过我实测目标环境已经很少有EdgeHTML了,所以只做了降级处理:如果检测到blob.slice存在但行为异常,就回退到mozSlice或webkitSlice,最坏情况直接禁用断点续传功能。
5.2 IE11老环境的降级方案
内部网络最大的变数永远是那些还在运行的老浏览器。我这次测试发现目标环境里还有不少IE11终端,Flash没了之后,WebUploader原版在IE11里基本失去了主力通道,只能退化成普通表单上传。
IE11不是完全不能用HTML5,它支持File API和Blob.slice,也支持XMLHttpRequest,但缺两个关键能力:一是缺少FormData对Blob的直接上传支持(有些机器上可行,但不稳定),二是缺少Service Worker这类现代恢复机制。我最终的决定是:IE11只提供普通表单上传,文件大小超过500MB直接在前端提示“当前浏览器不支持超大文件,请使用Chrome/Edge”。这个妥协在内部系统中是可以接受的,因为这个系统的核心用户都会收到统一的浏览器规范要求。
另外要提防的是IE11对中文文件名编码的处理,直接放到FormData里经常出现乱码,稳妥做法是前端统一用encodeURIComponent编码文件名,服务端再解码存储。
5.3 一次抓包排查的完整过程:上传中途连接被服务端重置
改造后第一次稳定运行测试时,遇到了一个比较棘手的问题:某个7GB的视频文件传到60%左右,所有分片请求突然全部报错,浏览器控制台显示ERR_CONNECTION_RESET。
一开始我怀疑是前端代码的问题,反复看调度逻辑没发现异常。后来打开DevTools的Network面板,发现所有报错的请求都是同一个特征:请求已经发出去很久,服务端一直没有响应,然后连接被RST重置。又去翻了服务端日志,发现Nginx在传输中途主动断开了连接。
进一步排查发现,单位内部用的Nginx配置里有两个默认值踩了坑:一是client_max_body_size默认1MB,但我们是分片上传,分片只有2MB,理论上不应该被拦;真正影响的是proxy_request_buffering默认开启,Nginx会把请求体完整缓冲到临时文件再转发,而临时文件所在分区空间被其他日志占满了,导致写入失败,Nginx直接断连。
解决方案是:在上传接口的location配置里加上proxy_request_buffering off;,同时单独给上传临时目录挂了一块独立磁盘,避免和日志分区争抢空间。改完后再跑大文件测试,再也没有出现过中途断连的情况。
这个排查过程给我的教训是:大文件上传出问题,不能只盯着前端,服务端反代、临时目录、磁盘空间、连接超时这些因素全都可能成为瓶颈。前端能做的就是把错误信息原样暴露到控制台和日志里,方便后端排查。
6. 工程化交付的几个建议与后续优化方向
改造完成并稳定运行一段时间后,回头总结,我觉得有三件事值得在交付前做好。
第一件事就是自定义构建。WebUploader的完整包很大,里面包含Flash Runtime、HTML5 Runtime、各种UI组件,如果直接引入全量包,光初始化就多出来几百KB的JS。在Gruntfile里把不需要的Runtime和组件剔除,只保留HTML5和UI相关的核心代码,体积能砍掉一半以上。我在项目里就是这么干的,产生的产物放在一个独立目录里,不和官方包混淆。
第二件事是服务端接口的幂等设计。分片上传接口、合并接口、check接口,这三者都要保证幂等。分片重复上传,同一分片后面的覆盖前面的,服务端要有唯一索引约束;合并接口被重复调用,不应该生成两份归档文件。这些看起来是后端的事,但前端在设计请求参数时要提前约定好兼容方式,尤其fileId的格式要全局唯一。我是用“文件大小+文件MD5前16位+随机短串”组合出来的fileId,既具备业务可读性,又能降低碰撞概率。
第三件事是做好上传过程的可观测性。我在组件里加了一个调试面板,把当前队列里每个分片的状态、重试次数、每片耗时、服务端返回结果全部显示出来。这个面板在测试阶段帮了大忙,很多和时序相关的问题,看一眼状态流转就清楚了。
后续如果要继续优化,我觉得可以从两个方向入手:一是把MD5算法换成兼容国密SM3的实现,在涉密级别更高的系统里,自定义哈希比MD5更稳妥;二是增加“服务端文件去重”逻辑,多个用户上传同一份卫星视频素材时,直接复用已有文件,不做多余传输。上传这个功能,用户平时不会夸奖你,但如果传一个几十GB的文件传到一半崩了,那印象就深刻了。我个人强烈建议先把校验链路做扎实,再考虑UI美化,因为上传功能的第一口碑永远是能不能完整地把文件传上去。
