
接这个需求的时候我正在维护一个给一线业务人员用的内部资料上传系统。业务方提出的要求很直接卫星视频文件单文件动辄几个GB要能传到服务器上不能被浏览器限制卡死中途断网、断电、误关页面重新打开后还得接着传不能重来最后这个功能要能在多种浏览器里稳定跑——包括老旧的IE内核浏览器以及内网环境下的各种双内核浏览器。任务落到我头上核心就一句话基于JS改造成熟的WebUploader把分片、断点续传、跨浏览器兼容做成一个可复用的上传插件。我先说结论WebUploader老归老但它的底层模型底子还在结合现代JS能力做一次认真改造完全能撑起这类高要求场景。这篇就完整拆解我的改造思路、关键代码、参数测算以及踩过的那些坑。1. 卫星视频上传的四个硬性约束先摸清战场再动手1.1 单文件体积经常突破10GB常规上传方案直接出局卫星视频和普通短视频完全不是一个量级。普通网盘上传单个文件几百MB到1GB已经是极限体验但卫星视频素材的原始文件经常是几GB甚至几十GB而且格式偏专业多是TS、MP4、MXF这类封装。浏览器里用最原始的input typefile加普通表单Post传到一半连接超时直接失败用户心态崩了服务器也扛不住。有一个关键认知得先纠正浏览器本身不限制上传文件大小multipart/form-data协议也没有硬性体积上限真正的限制来自HTTP连接稳定性、服务器接收超时时间、以及浏览器在长时间请求中的内存表现。一个5GB文件如果走单请求上传一个连接要持续几十分钟甚至更久中间任何一次网卡切换、代理超时、服务器网关超时都意味着整文件重来。分片上传是唯一可行路线。1.2 上传中断率极高网络环境并不可靠军工行业的业务网络网络稳定性不像互联网产品那么理想。很多一线场站的网络是专线加卫星链路组网带宽波动大延迟高偶尔还会断流。用户把视频传到一半网络闪断如果方案不支持断点续传恢复后还得从头传操作成本没法接受。断点续传的核心价值不是省流量是省用户的操作时间。一个十几GB的文件断一次可能已经传了3个小时重传意味着这些时间全部作废。所以续传不能停留在“重新选文件再传”的层面要能在页面加载后自动检测未完成任务给用户一个“继续上传”的入口甚至可以直接自动续传。1.3 浏览器环境高度碎片化跨浏览器不是装饰词卫星视频业务的终端环境比互联网产品复杂得多。一线值班室可能还用着Windows 7配老版Chrome机关办公区可能是双内核的国产浏览器也有用户坚持用Edge或Firefox。前端工程师最怕的不是现代浏览器而是那些“看起来是Chrome内核但版本极老”的浏览器以及IE11这类已经停止支持但仍在内网运转的顽固派。跨浏览器兼容不是一个开关是一个持续投入的工程问题。WebUploader当年设计了一个很优雅的降级方案优先用HTML5不支持就用Flash。现在Flash已经彻底退出历史舞台我们再怎么改造也不可能把Flash捡回来。所以目标要重新定义为在一切支持HTML5 File API的浏览器里跑得稳在不支持的环境给明确提示。1.4 内网部署与敏感数据的特殊约束军工行业业务系统的部署环境有很强的特殊性系统跑在隔离内网不能访问外网CDN无法在线引jQuery和WebUploader的公共库所有静态资源必须随系统离线打包上传的数据本身有敏感性传输通道和存储都要符合安全要求业务系统经常是多个子系统共存上传组件不能和现有页面样式、脚本库发生冲突。这一点直接决定了改造方案的另一个关键设计插件必须零外部依赖自包含。不能依赖外网CDN也不能假设宿主页面已经引入jQuery。WebUploader官方版本是依赖jQuery的改造时要么把jQuery一起内嵌要么深入源码做依赖解耦。我选择了后者把WebUploader的依赖从jQuery中剥离换成自己实现的轻量事件系统和DOM工具这样插件在任何页面里都能直接加载。2. WebUploader原生能力的边界它到底缺了什么2.1 原生分片机制有但不够细WebUploader原生支持分片上传在初始化配置里设置chunked: true和chunkSize就能启用。它的处理方式是将文件按固定大小切成多个分片逐个上传带到最后一并合并。单看这个逻辑方向是正确的但实际用起来有几个突出问题。首先原生的分片信息管理过于简陋每个分片只是简单标记“上传中”和“上传成功”没有分片的持久化记录。页面一刷新内存里的状态全部丢失续传完全依赖服务端做额外的去重和校验但WebUploader官方提供的Server示例并没有完整实现这一套等于把最难的活留给了使用者。其次原生并发控制能力弱。WebUploader有threads参数控制并发数但它的调度策略偏向简单队列对大文件场景下“当前传输速率估算”“动态调整并发”这类能力完全没有。而且它在并发上传时每个分片都会创建独立的XMLHttpRequest不受withCredentials统一管控遇到需要带Cookie或Token的认证场景很容易出问题。最后原生分片的失败重试机制也比较粗糙。某个分片上传失败后默认行为是整体标记失败而不是自动重试该分片。在弱网环境下这会频繁打断用户操作体验很差。2.2 断点续传的实现盲区断点续传在WebUploader原生体系里更像一个概念不是完整的解决方案。它能做到的仅限于上传过程中如果某个分片失败再次选择同一个文件可以跳过已上传的分片。但它有几个致命盲区。一个盲区是文件标识不稳定。WebUploader默认用文件名加文件大小作为唯一标识这对普通文件问题不大但对视频素材同名文件不同内容是常见的事。一旦文件名和大小都相同系统就会误判为同一文件产生错误的秒传。另一个盲区是续传上下文完全丢在浏览器内存里。用户上传到一半浏览器崩溃或标签页被关闭再打开页面时WebUploader实例已经重新创建之前的内存状态全部没了。如果没有一个持久化的任务存储所谓断点续传其实根本不成立。还有盲区是续传校验依赖服务端无条件信任。客户端说“我已传完这些分片”服务端如果没有实际校验可能出现分片损坏、缺片漏片但服务端仍然返回成功最终合并出来的视频文件花屏、无法播放。2.3 兼容性策略还停留在Flash时代WebUploader的另一个经典设计是运行时策略runtimeOrder默认是html5,flash。在它诞生的年代这是很聪明的方案HTML5能力不足时用Flash补齐。但2024年了Flash早就被浏览器彻底封杀任何环境都不应该再尝试加载Flash。如果不改造系统里只要保留了Flash的Runtime检测代码在部分禁用插件的老浏览器上会报无意义的错误。改造时我直接把Flash相关的Runtime全部移除把HTML5作为唯一运行时。同时针对HTML5 Runtime中一些老浏览器实现不标准的API比如File.slice的兼容写法对它们做一层polyfill和降级处理。2.4 改造方向与插件化拆分分析完WebUploader的原生边界改造方向就很清晰了保留它成熟的上传队列管理和UI事件机制但把底层传输层重写拆成“任务管理”“传输执行”“状态存储”“兼容适配”四个独立模块。将上传任务的持久化从内存搬到浏览器本地存储并且和服务器任务状态做双向同步。把整个能力封装为一个独立的JS插件对外暴露简单的配置项和回调方法内部自己管理WebUploader实例、分片队列、断点信息。插件化的意义在于后续不管哪个业务系统要接入不需要理解WebUploader的内部细节只需要引入插件文件配置几个参数就能获得完整的卫星视频上传能力。3. 分片参数怎么定分片大小、并发数与内存占用的权衡3.1 分片大小的计算公式与实测对比分片大小是断点续传的基石。片切得太小分片数量太多每次请求的固定开销HTTP握手、传输头、服务端分片元数据记录比例增大整体效率反而下降。片切得太大又回到了单请求上传的困境一个大分片传输失败重试成本很高。通用的经验公式是单分片大小 预估带宽 × 5~10秒。假设当前链路的稳定上传带宽是5MB/s那单分片大小在25MB到50MB之间比较合理。我实际测试下来内网链路稳定在20MB/s以上时32MB的分片表现很好如果带宽降到2MB/s会相应降低分片大小。以下是我在同等网络条件下用一个4.7GB的卫星视频文件做的分片大小对比测试分片大小分片数量总耗时失败重试次数内存峰值体验评价2MB2400约11分钟42中等请求数量过多服务端压力大失败率高8MB600约8分钟18低中规中矩无明显缺陷32MB约150约6.5分钟6较低综合最优服务端压力小失败少64MB约75约7分钟9高单个分片传输时间长失败后重试代价大128MB约37约8分钟12高接近单文件上传弱网下体验差最终选型用了32MB。对于更大的文件比如50GB以上我建议把分片上限继续放宽到64MB但内存优化要做好。分片文件从File对象中slice出来后能否及时释放引用直接影响内存表现。3.2 并发数设置带宽、浏览器连接数与服务器压力并发数的确定比很多人想的复杂。WebUploader原生threads参数默认是3但这个值在宽带高、文件大的场景下明显太保守。不过并发也不是越大越好受两个现实约束第一浏览器的同域名连接数限制。HTTP/1.1协议下浏览器对同一域名的并发连接数通常是6个。超过这个数请求会被排队并发提升并不会带来实际吞吐量的增长。如果系统上了HTTP/2连接数限制会宽松很多但内网系统部署HTTP/2的比例并不高。第二服务端的并发处理能力。每增加一个并发意味着服务端同时多开一个分片接收通道。如果服务端逻辑里有全局锁或者单线程合并逻辑高并发反而会造成大量请求等待超时。我的经验值是HTTP/1.1环境下并发设为4到5给其他业务的API请求留出连接余量HTTP/2环境下可以适当提高到6到8。同时引入一个简单的动态并发调节机制根据最近10个分片的平均传输耗时和失败率自动微调并发数。如果失败率超过阈值降低并发优先保证稳定上传而不是一味求快。3.3 分片序号与合并策略分片的序要从0开始连续编号合并时严格按序号顺序拼接。这里有个容易被忽略的细节服务端不能假设分片按顺序到达。因为并发上传的多个分片是异步传输先发的分片不一定先到。服务端必须先把每个分片写入独立存储等所有分片齐全后再按序号合并。实现时我在分片上传请求中携带了三个关键参数{ fileId: xxxx-xxxx-xxxx, // 文件唯一标识 chunkIndex: 0, // 当前分片序号 totalChunks: 150 // 总分片数 }服务端在接收到分片后用fileId chunkIndex作为存储Key写入临时目录。当收到的分片数量等于totalChunks时触发合并流程。合并采用流式读取写入而不是把所有分片一次性读入内存因为大文件的分片总量可能很大一次性读取会导致服务端内存溢出。3.4 超大视频的抽样校验全量MD5不可行文件完整性校验是断点续传中很容易出错的一环。很多人第一时间会想到对文件做整体MD5然后在服务端比对。对大文件来说这个方案极其不现实单个10GB的文件前端计算MD5可能需要几十秒甚至几分钟占用大量CPU和内存用户等待时间过长体验非常差。我做的是抽样校验加首尾块校验。具体策略是读取文件前1MB、中间1MB、最后1MB的内容计算哈希值。整个文件的文件大小、文件名、修改时间作为附加特征。合并这四个参数生成fileId。这样计算速度几乎无感知又能避免“同名同大小但内容不同”的误判。当然抽样校验在极端情况下仍有碰撞可能前后中三个块都完全相同的概率极低所以我额外加了一个兜底机制合并完成后服务端对合并文件做一次快速一致性比对比对局部块哈希不一致则标记为失败并删除这份合并结果要求客户端重新上传缺失分片。注意抽样校验不是银弹。如果业务对文件完整性有极高的要求比如原始素材必须与源文件字节级一致那一版方案里还是应该提供“可选的全量校验”开关让用户在首次上传时选择是否执行全量哈希计算。虽然慢但能换来万无一失。4. 断点续传的命脉文件指纹计算与上传状态恢复4.1 文件指纹怎么在不读取整个文件的情况下保证大概率唯一前面提到的抽样哈希就是文件指纹。这个指纹在断点续传里扮演“身份证”的角色用户重新选择文件时插件在本地重新计算指纹然后去服务器查询这个指纹是否存在。设计指纹方案时我还有过一个纠结到底用文件名大小最后修改时间还是引入内容哈希最终内容哈希方案胜出因为文件名和修改时间不可靠——用户可能把satellite_20241012.mp4重命名成final_v2.mp4大小一样但内容不同如果只靠名称和大小匹配续传时就会接到一个错误的任务上合并出来的视频完全不能用。内容抽样哈希因为计算速度极快用户几乎没有感知。用Web Crypto API的crypto.subtle.digest计算哈希不需要额外引入加密库现代浏览器都支持。老浏览器不支持时用SparkMD5作为降级方案。4.2 断点信息存哪里localStorage、IndexedDB还是服务端断点信息必须持久化但持久化的位置需要权衡。我同时用了三层存储各司其职localStorage存储轻量级的任务摘要列表包括fileId、文件名、文件大小、总片数、上传时间等。优点是读取快同步API用起来简单。缺点是5MB容量限制不能存大数据不能存二进制。IndexedDB存储每个分片的上传状态、上传进度、以及上传过程中的元数据。比如已传分片序号列表、每个分片的校验值、当前使用的分片大小。IndexedDB容量大、支持异步读写适合存这种结构化数据。服务端数据库最权威的任务状态存储。客户端上传每个分片前先向服务端查询任务状态上传分片成功后服务端更新数据库。页面刷新后客户端从服务端拉取任务快照结合本地IndexedDB信息恢复上传队列。三层存储的好处是各取所长LocalStorage负责快速展示IndexedDB负责恢复上下文服务端兜底保证无论浏览器清理了本地数据只要服务端还有任务记录用户依然可以续传。4.3 续传核心流程秒传判断、已传列表校验、断点恢复续传的完整流程可以拆成三个层级第一步秒传判断。用户选择文件后插件计算文件指纹然后调用服务端/checkFile接口。服务端如果发现该指纹的任务已存在且状态为“已合并完成”直接返回“秒传成功”用户无需等待上传。这个场景常见于用户重复上传同一文件节省的不只是流量更是用户操作时间。第二步已传分片列表校验。如果服务端任务状态是“上传中”返回该任务已收到的分片序号列表。客户端拿这个列表和本地记录的已传分片做比对取并集。同时为了应对服务端存储分片损坏的情况客户端在续传时会随机抽取几个已传分片调用服务端的校验接口做内容哈希比对如果发现不一致把这个分片标记为“需重传”。第三步断点恢复。客户端从缺口分片开始继续上传已传分片直接跳过。恢复完成后触发服务端合并流程。这三步流程看起来简单但有一个关键前提客户端和服务端的任务状态必须保证最终一致。如果客户端认为分片3已传完服务端也返回已收到但实际存储的分片3数据是损坏的那合并时就出问题。所以我在分片校验逻辑里加入了随机抽检虽然增加了少许请求开销但能显著提升最终合并文件的可靠性。4.4 服务端接口设计与任务表结构断点续传不是纯前端就能搞定的事服务端接口设计直接影响客户端逻辑的复杂度。我的服务端提供了一组RESTful接口接口方法作用核心参数/api/upload/checkFilePOST查询文件是否已存在返回是否秒传、已传分片列表fileId/api/upload/initPOST初始化上传任务返回任务ID和分片上传凭证fileId,fileName,fileSize,totalChunks/api/upload/chunkPOST上传单个分片taskId,chunkIndex,file/api/upload/mergePOST触发服务端合并分片taskId,fileName/api/upload/progressGET查询任务当前进度taskId/api/upload/cancelPOST取消任务清理临时分片taskId对应到服务端数据库核心表结构大概是这样CREATE TABLE upload_task ( id VARCHAR(64) PRIMARY KEY, file_id VARCHAR(64) NOT NULL, file_name VARCHAR(255) NOT NULL, file_size BIGINT NOT NULL, total_chunks INT NOT NULL, chunk_size INT NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0:上传中 1:已合并 2:失败 created_at DATETIME, updated_at DATETIME ); CREATE TABLE upload_chunk ( id BIGINT AUTO_INCREMENT PRIMARY KEY, task_id VARCHAR(64) NOT NULL, chunk_index INT NOT NULL, chunk_hash VARCHAR(64), uploaded_at DATETIME, UNIQUE KEY uk_task_chunk (task_id, chunk_index) );task_id通常在init阶段生成客户端后续所有请求都携带它。状态字段status在合并成功后置为1任何查询秒传时都会检查这个字段。5. 跨浏览器兼容的具体落法适配层与回退策略5.1 File.slice兼容性梳理改造跨浏览器兼容最先要面对的是File.slice方法。这个API在W3C规范里多次改名老版本浏览器实现的是File.slice和File.mozSlice、File.webkitSlice。方法名不同行为也有差异。我的处理方案是在插件初始化时做一次能力探测统一封装成内部方法const fileSlice (file, start, end) { if (file.slice) { return file.slice(start, end); } if (file.mozSlice) { return file.mozSlice(start, end); } if (file.webkitSlice) { return file.webkitSlice(start, end); } throw new Error(当前浏览器不支持文件分片操作); };注意end参数在老实现里的语义是“偏移量”还是“绝对位置”不同实现不一致。统一封装后用绝对位置语义所有调用都走这个方法避免底层差异泄漏到业务代码里。5.2 WebUploader底层替换从Flash到HTML5的收尾WebUploader源码里Runtime的加载逻辑默认会依次探测HTML5、Flash、HTML4。Flash已经退出历史舞台但在某些老浏览器上探测代码仍会尝试加载Flash的JavaScript文件此时如果文件不存在就会抛加载错误。改造时需要把源码里flash相关的Runtime代码彻底移除或者至少在运行时配置里禁止探测。做法是在配置项中强制指定runtimeOrder: html5同时在代码层面把加载Flash资源的路径全部注释掉避免任何无效请求。另一个容易忽略的点是HTML5 Runtime内部的兼容分支。WebUploader在检测到不支持FileReader、FormData等API时会降级为HTML4模式也就是普通表单上传——那对分片上传等于没有。所以我在适配层增加了一个前置检测如果核心API缺失直接阻断并提示用户更换浏览器而不是让插件悄悄降级成不可用的状态。5.3 双内核浏览器的特殊处理内网环境中常见的双内核浏览器比如某数字浏览器、某安全浏览器会在“极速模式”和“兼容模式”之间切换。极速模式走Chromium内核现代API基本都支持兼容模式走IE内核很多API缺失。双内核浏览器在切换内核时会重新加载页面这会导致已经初始化好的上传任务中断。处理办法是在页面加载时检测当前浏览器模式和内核类型如果处于兼容模式给用户一个明确的指引引导切换到极速模式同时在应用层把上传插件的入口单独做一个页面即使切换内核导致页面重载上传任务也能靠持久化状态恢复。我写了一个简单的浏览器环境检测函数输出当前内核信息function detectBrowserInfo() { const ua navigator.userAgent; let kernel unknown; let isCompatibleMode false; if (ua.includes(Trident) || ua.includes(MSIE)) { kernel ie; isCompatibleMode true; } else if (ua.includes(Chrome)) { kernel chrome; } else if (ua.includes(Firefox)) { kernel firefox; } // 双内核浏览器通常会在UA中暴露Trident或兼容模式标记 return { kernel, isCompatibleMode }; }这只是一个简化版本实际项目中我还结合了document.documentMode这个属性在IE内核下会返回版本号在Chrome内核下是undefined是判断兼容模式的一个有效手段。5.4 适配层设计把浏览器差异关在插件内部适配层是插件里最重要的设计之一。我把所有浏览器相关的API调用都收敛到一个适配模块中对外暴露统一接口内部处理差异。比如上传请求的发送我封装了createXhr方法统一处理withCredentials、请求头、超时和错误状态映射。再比如文件读取封装了readArrayBuffer统一处理FileReader和Blob.arrayBuffer()的差异。这样做的收益是后续如果发现某个新浏览器的兼容问题只需要在适配层里修一次不需要动上层业务逻辑。适配层的另一项重要职责是事件行为的归一化。不同浏览器对上传进度事件、错误事件、表单数据序列化的实现略有差异把这些差异统一后上层代码才能有一套稳定的状态机。6. 军工内网场景的插件化封装离线部署与安全适配6.1 插件整体架构初始化配置、事件回调、生命周期插件化的目标是让使用者不需要理解内部实现只需要通过配置和回调就能接入。我的插件对外暴露了一个工厂方法const uploader createVideoUploader({ container: #upload-container, // 挂载点 chunkSize: 32 * 1024 * 1024, // 分片大小 threads: 4, // 并发数 maxFileSize: 50 * 1024 * 1024 * 1024, // 最大支持50GB url: /api/upload, accept: [.mp4, .ts, .mxf], autoStart: true, // 选择文件后自动上传 onUploadStart(fileId, fileName) {}, onUploadProgress(fileId, percent) {}, onChunkSuccess(fileId, chunkIndex) {}, onUploadComplete(fileId, fileUrl) {}, onUploadError(fileId, error) {}, onTaskRestore(taskList) {} // 页面刷新后恢复任务列表 });插件内部的生命周期清晰可控初始化时加载配置、创建WebUploader实例、注册事件用户选择文件后进入指纹计算阶段指纹计算完成后进入秒传判断和续传判断然后进入分片上传阶段最后合并完成触发完成回调。整个过程可以随时被取消或重置。6.2 离线资源与无CDN依赖内网环境不能访问外网CDN所有静态资源必须随系统一起发布。插件包在构建时把所有依赖打入一个文件WebUploader源码、适配层代码、上传逻辑代码、以及所有样式。插件文件本身不发起任何外部请求这是底线要求。另一个常见问题是字体文件和图标资源。如果插件里有使用字体图标构建时必须内嵌为Base64或者改用SVG图标否则部署到内网后字体加载不出来界面会变得残缺难看。由于宿主页面可能没有jQuery我的插件在构建时把WebUploader依赖的jQuery子集功能做了内联实现。这样无论宿主页面是否引入jQuery插件都能独立工作。这一步是WebUploader改造中最繁琐的部分但完成后插件的可移植性大大提升。6.3 国产化环境的兼容打磨所谓国产化环境在实际项目中指两类一类是搭载国产操作系统的终端浏览器多为Firefox ESR版本或基于Chromium的定制版本另一类是基于国产CPU的服务器和终端性能相比x86环境弱一些。针对这些环境的适配经验是压低默认并发数避免大文件读取卡死主线程。国产终端的内存和CPU资源相对紧张我在检测到低内存设备时默认并发降到2分片大小降到16MB。同时把文件读取改成异步分块读取避免一次性把大文件切片读到内存里造成系统假死。另外国产浏览器对crypto.subtle等API的支持情况参差不齐我在指纹计算时做实时降级如果crypto.subtle不可用自动切换到SparkMD5方案保证任何环境都能生成文件指纹。6.4 日志、监控与灰度开关大文件上传功能上线后最怕的是用户说“传失败了”但没有任何线索。我的插件内置了上传日志模块所有关键节点都记录日志文件选择、指纹计算、任务初始化、每个分片的开始和结束、失败原因、服务器返回状态码等。日志默认存储在IndexedDB中每条日志包含时间戳、文件ID、事件类型、详细描述。为了不和宿主页面的业务逻辑耦合日志只记录技术上关键的信息不记录视频文件名、内容等敏感信息。这一点在数据安全要求高的场景尤其重要——排查问题要的是技术细节不是业务内容。监控方面我用一个简单的上报接口把统计信息发给运维平台包括上传成功率、平均传输速率、分片失败率、常见失败原因分布。结合灰度开关可以按用户组、按文件大小区间逐步放量避免一次性全量上线把问题暴露给所有用户。7. 踩坑实录连续上传中断、内存暴涨与服务端丢片7.1 问题一上传到99%页面崩溃内存持续增长第一个严重问题是在测试一个16GB的视频时出现的。上传到99%浏览器标签页直接崩溃任务管理器显示Chrome内存占用超过3GB。这个问题的根因不在WebUploader传输层而在分片读取和Blob引用释放。排查链路是这样的先把上传功能还原到最小复现场景确认问题稳定出现然后用Chrome Performance面板录制内存曲线发现内存曲线在分片上传阶段呈阶梯式上涨每上传一个分片内存涨30到50MB但上传完成后不回落结合Memory面板的堆快照发现每个分片都生成一个ArrayBuffer但上传结束后的回调闭包里仍然持有这个ArrayBuffer的引用导致无法被垃圾回收。解决方案是两个一是把分片的上传逻辑改成“读取→上传→置空引用”的严格时序确保每次循环结束局部变量不保留分片数据引用二是在上传分片时用Blob.slice获取分片后直接传给FormData不额外转成ArrayBuffer减少一次大对象复制。const uploadChunk (task, chunkIndex) { const start chunkIndex * task.chunkSize; const end Math.min(start task.chunkSize, task.file.size); const chunkBlob fileSlice(task.file, start, end); const formData new FormData(); formData.append(file, chunkBlob, ${task.fileId}_${chunkIndex}.part); formData.append(taskId, task.taskId); formData.append(chunkIndex, chunkIndex); // 上传结束后显式释放引用 return xhrUpload(formData).finally(() { chunkBlob null; // 这里在真实代码中应通过作用域设计保证引用释放 }); };提示在生产代码里把chunkBlob置空这种写法仅用于强调意图真正要解决问题是保证这个变量不在任何持久化的闭包中被捕获。上传回调里不要引用整个分片对象。7.2 问题二续传后服务端拼接视频花屏、音画不同步续传功能上线后有用户反馈说续传完成的视频播放时花屏、音画不同步。一开始怀疑是分片合并顺序问题检查后确认合并顺序没错分片序号和文件偏移是对得上的。继续深挖发现是快传校验接口里没有做分片内容完整性哈希校验某个分片在第一次传输过程中实际已经损坏但客户端和服务端都认为它上传成功了。续传时这个损坏的分片被跳过合并后就出现了花屏。解决措施是前面提到的随机抽检机制续传时对已传分片做内容哈希比对不一致的强制重传。同时对合并完成后的文件做整体校验花屏问题彻底消失。这个坑给了一个重要教训分片上传成功不能只以“服务端收到请求”为准必须以“服务端校验内容通过”为准。尤其对视频这类对内容完整性敏感的文件任何环节的信任都可能变成播放时的花屏。7.3 问题三Edge与Chrome行为不一致同一个浏览器内核却表现不同测试过程中还遇到一个诡异问题同一份代码Chrome里跑得好好的Edge里偶尔会出现分片上传请求被挂起、迟迟不发出。对比网络面板后发现Edge对同域名并发连接数的限制更严格而且当连接池占满时新请求的行为不是排队而是静默挂起。排查过程排除了服务端问题因为同样请求在Chrome里能正常返回。最后确认是Edge的HTTP/1.1连接池行为差异。解决办法有两个一是把默认并发数从5降到4给其他请求留出余量二是为上传请求单独指定一个子域名比如up.example.com让上传请求不占用业务请求的连接额度。在军工内网环境子域名如果不方便就只能在插件内加“请求超时检测”超过一定时间未发送的请求主动取消并重试。7.4 问题四跨域上传被拦截上传组件与业务系统跨域隔离内网系统经常是前后端分离部署上传接口和业务页面不在同一个域名下。Chrome下配置CORS后基本没问题但IE11和部分国产浏览器对CORS的支持仍不完善特别是withCredentials和自定义请求头的跨域处理。解决方案是统一走代理模式上传接口通过业务系统的反向代理转发前端只请求同域的代理路径服务端代理把请求转发到真正的上传服务。这样前端完全不需要处理跨域也规避了浏览器CORS差异。同时在服务端配置中显式允许上传组件的来源域名并且对所有写接口做CSRF防护。8. 后续扩展从单机上传到集群合并改造完成、稳定运行一段时间后我又面临一个可预见的演进需求当多个用户同时上传大文件单台服务器的磁盘和合并计算会成为瓶颈。这时候需要考虑把分片上传和合并拆成两个独立服务分片接收服务可以水平扩展接收完的分片先存到分布式对象存储合并任务由消息队列异步触发合并服务从对象存储拉取分片、按序合并、回写结果。这种架构还带来一个好处的即使合并服务在合并到一半时崩溃因为分片都持久化在对象存储里重新触发合并任务即可不需要客户端重新上传任何数据。这个设计思路是后续演进的重点但核心原则和单机版一致分片是幂等的合并是可靠的状态是持久化的。我个人的实际体会是改造WebUploader这类老库最难的不是写新功能而是先搞清楚旧代码的边界。WebUploader的分片上传模型、压缩队列管理、事件系统都设计得很好只要不是推翻重来而是在它基础上做减法去掉Flash、补短板持久化状态、加能力指纹、抽检、动态并发就能又快又稳地交付一套能应对工业级场景的上传插件。这套改造思路不只适用于卫星视频任何GB级文件、断点续传、跨浏览器需求的场景都可以直接参考这套方案。