我接触WebUploader这个插件,是在一个内网视频数据管理项目里。服务器上每天要接收几十路卫星遥感视频,单段文件动辄十几个GB,通过浏览器上传到内网平台。原生的WebUploader版本在公网普通文件场景下很好用,但拿来做超大视频文件、还要跨浏览器、还要断点续传,坑确实不少。折腾了两三个月,我把整个上传组件从“能用”改到“内网环境稳定跑几个月不崩”,这篇文章就把改造过程中踩过的坑、用到的方案、以及可复现的代码逻辑完整记录下来。
文章适合两类人看:一类是正在用WebUploader做文件上传,发现大文件传不动、刷新就断的人;另一类是在内网或专业场景里做跨浏览器上传组件选型、想了解分片断点续传背后原理的前端工程师。读完你至少能理清四件事:分片怎么设计才合理、断点续传到底断在哪、跨浏览器兼容要补哪些代码、以及服务端该怎么配合才能落地。
1. 项目背景与需求分析
1.1 卫星视频文件上传的真实痛点
先别急着写代码,得把场景说清楚。这个系统里的视频文件有几个特点:
- 体积大:单段视频从几百MB到几十GB不等,峰值时段一小时以内要上传几十个文件,总吞吐量在几百GB级别。
- 时效高:卫星过境拍摄的视频数据要求尽快上传到处理平台,供后续判读和分析使用,不能等太久。
- 内网环境带宽不稳定:用户单位虽然都是内网专线,但链路质量受天气、设备、中间节点影响,经常出现传输中断。
- 浏览器环境杂:用户客户端有Chrome、Firefox、360安全浏览器、QQ浏览器,还有一些双核浏览器,甚至个别机器还在用老版本的Edge。
如果用最传统的<input type="file">直接POST整个文件到服务端,遇到几百MB的文件还好,遇到十几GB的文件就是灾难:一个请求挂在那里,中途网断了全部重来,浏览器内存也扛不住。项目组早期的原型就是这么做的,测试时传一个4GB文件,等了四十分钟,然后网断了,全部白传。这就是典型的“没有分片、没有断点”的教训。
1.2 原生WebUploader的局限在哪里
WebUploader是百度开源的一个上传组件,支持HTML5和Flash两种上传方式,封装了文件选择、队列管理、进度回调和并发控制。但原生版本并不能直接解决我们这里的问题,原因有三:
第一,H5分片上传的断点续传不完整。WebUploader分片是按file.id在内存里维护的,刷新页面后内存清空,断点信息全部丢失。实际测试时,一个6GB文件上传到一半,页面一刷新,又从头开始分片。它没有把“哪些分片已经上传成功”持久化到服务端,所以严格意义上只能叫“分片上传”,不能叫“断点续传”。
第二,对超大文件的内存处理不友好。WebUploader底层通过FileReader读取分片数据,默认分片大小如果设得过大,一次性读入内存的数据量会非常夸张。把这个参数调到几十MB,并发数再一上去,浏览器直接崩溃。
第三,跨浏览器的兼容层不够彻底。原生版本在Chrome、Firefox上跑得很顺,但在国产双核浏览器(特别是兼容模式)和老版本Edge下,Blob.slice方法名不一致,甚至有些内核不支持FormData构造。这些细节导致了“这个系统只适配了Chrome”的尴尬。
1.3 改造目标与技术路线
基于这些痛点,我把改造目标定为以下五点:
- 支持跨浏览器:覆盖Chromium内核、Firefox、WebKit内核及部分老旧浏览器,至少保证核心上传流程可用。
- 支持超大附件:单文件最大支持到50GB,页面不崩溃、内存可控。
- 支持断点续传:上传中断后,刷新页面或重启浏览器,能够从未完成的分片继续传,而不是从头开始。
- 支持暂停与恢复:用户可主动暂停,稍后继续上传。
- 支持进度可视化与失败重试:每个文件、每个分片都有明确状态,失败分片能自动重试。
技术路线选择上,我保留WebUploader作为队列管理和进度回调框架,但把它的分片策略、上传请求逻辑和文件读取方式做深度改造,同时配套一套服务端接口规范。WebUploader的优势在于它已经把选择文件、拖拽上传、队列状态这些流程封装得非常完善,我们不需要重复造轮子,只需要替换它的“传输引擎”部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术方案设计:分片与断点续传机制
2.1 分片大小怎么定才能兼顾速度与稳定
分片大小是整个改造里最先要确定的核心参数。分片太小,请求数量变多,服务端压力大,总耗时反而变长;分片太大,单次读取内存占用高,网络出错概率变大,重试成本高。
我用一个简单公式来估算:
text复制分片大小 = 单条TCP连接的有效吞吐率 × 期望每个分片传输时长
假设内网单条连接实际吞吐率约5MB/s,期望每个分片在5-10秒内传完,那分片大小就是25MB-50MB?这个数字在实际测试中发现并不可行,因为浏览器端FileReader读取一个50MB分片再通过XHR发送,会产生明显的内存峰值,并发两个分片就可能占用300MB以上内存。
最终经过多轮测试,我选了5MB作为默认分片大小,原因很直接:
- 5MB分片在任何主流浏览器里都能稳定读取,内存占用可控;
- 一个10GB文件切分成2000个分片,对服务端接口的压力在可接受范围;
- 网络波动导致某个分片失败时,重传5MB的成本极低;
- 上传进度能精确到0.5%,进度条视觉上非常平滑。
如果你的场景网速更快、服务端配置更高,可以考虑加大到10MB,但不建议超过20MB。分片大小不是越大越好,它要跟并发数、内存和网络抖动频率做权衡。
2.2 每个文件需要一个全局唯一的标识
断点续传的前提条件,是服务端必须知道“现在上传的这个文件,跟昨天传了一半的文件是不是同一个”。所以前端必须为每个文件计算一个唯一标识。
常见的方案有两个:
方案一:完整文件MD5。 使用SparkMD5增量计算整个文件的哈希。优点是唯一性最强,甚至可以支持“秒传”功能;缺点是超大文件哈希计算非常耗时,一个10GB文件全量计算MD5可能需要几分钟,在低配客户端上会更久。
方案二:特征值组合。 取文件的“名称+大小+最后修改时间+前2MB分片的MD5”作为唯一标识。优点是计算快,基本是毫秒级;缺点是极端情况下可能出现标识碰撞,比如同名同大小同时修改的文件被替换内容。
我采用的是折中方案:在前端使用SparkMD5计算整个文件的MD5,但提供“仅计算前32MB”的配置选项。对于卫星视频这类文件量大的场景,完整MD5的等待时间不可接受,而前32MB加文件大小已经能保证极高的唯一性。具体做法是:
javascript复制// 伪代码:计算特征值标识
function calculateFileToken(file) {
const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
const chunkSize = 4 * 1024 * 1024; // 每段4MB
const maxReadSize = 32 * 1024 * 1024; // 最多读32MB
let bytesRead = 0;
let index = 0;
// 这里用SparkMD5.ArrayBuffer增量计算
let spark = new SparkMD5.ArrayBuffer();
while (bytesRead < maxReadSize && index < file.size / chunkSize) {
let start = index * chunkSize;
let end = Math.min(start + chunkSize, file.size, maxReadSize);
let chunk = blobSlice.call(file, start, end);
spark.append(chunk);
bytesRead += end - start;
index++;
}
return {
token: hex_md5(file.name + file.size + file.lastModified) + '_' + spark.end(),
size: file.size,
name: file.name
};
}
每次上传前,前端先把{token, size, name}发给服务端,服务端记录这个文件的上传状态。这样即使跨天、跨浏览器、跨机器(只要文件相同),也能精确找到之前上传过的分片记录。
2.3 断点续传的完整流程设计
断点续传不是一个前端功能,而是前后端配合的完整协议。我设计了一个三段式流程,每个阶段对应一个接口:
第一阶段:Check接口(判断文件状态)
上传前,前端携带文件标识访问服务端:
text复制GET /api/upload/check?token=xxx&size=xxx&name=xxx
服务端返回:
json复制{
"exists": false, // 是否已完整上传
"uploadedChunks": [1,2,3,5], // 已上传的分片索引
"chunkSize": 5242880, // 服务端记录的分片大小
"totalChunks": 2000
}
前端拿到uploadedChunks后,把已上传的分片直接从上传队列中删除,只排缺失分片。这个步骤是整个断点续传的核心,也是区别于普通WebUploader的关键。
第二阶段:Upload接口(上传单个分片)
接收到Missing分片后,前端按顺序发送分片数据。每个分片请求携带参数:
token:文件唯一标识chunk:当前分片索引(从0开始)chunks:总分片数size:文件总大小name:文件名
服务端收到分片后,把它写入临时目录,文件名格式为{token}_{chunk}.part。写入成功后返回200 OK。
第三阶段:Merge接口(合并分片)
当所有分片都上传成功,前端调用合并接口:
text复制POST /api/upload/merge
Body: { token, name, size, totalChunks }
服务端把{token}_0.part到{token}_{totalChunks-1}.part按顺序以二进制流方式合并成最终文件,合并完成后删除临时分片文件。
这套流程的好处是,每个阶段职责单一、易于排查问题。前端不管服务端是怎么存储分片的,只管“哪些没传、传哪些、传完通知合并”。
2.4 并发控制与重试策略
WebUploader原生支持threads参数控制并发上传数量。这个参数不是越大越好,原因有两方面:一是浏览器对同一域名的并发连接数有限制(HTTP/1.1默认6个),超出部分会排队等待,反而增加复杂度;二是并发分片过多,前端内存占用巨大,服务端连接数也吃不消。
我把它设为4,这是实测下来比较理想的值:三个上传任务同时跑,不会打满带宽,进度条依然流畅,服务端压力可控。
重试策略也不能无限重试。我设置的是每个分片最多重试3次,每次重试间隔2秒、4秒、8秒递增退避。如果分片连续失败3次,就暂停整个文件上传,标记为“失败”,由用户手工触发重传或在线路恢复后自动重试。这个策略参考了TCP拥塞控制的思路,避免失败时集中冲击服务端。
3. 跨浏览器兼容性改造细节
3.1 兼容范围与优先级
做跨浏览器改造之前,先把环境搞清楚。我统计了这个内网系统用户的浏览器分布,大概是:
| 浏览器类型 | 占比 | 内核 | 关键兼容问题 |
|---|---|---|---|
| 360安全浏览器(极速模式) | 30% | Chromium 78+ | 基本无 |
| 360安全浏览器(兼容模式) | 15% | IE 11+ Trident | File API不完整,需降级 |
| Chrome / Edge | 25% | Chromium 90+ | 基本无 |
| Firefox | 15% | Gecko | 偶发跨域限制 |
| 其他(QQ、搜狗等) | 15% | Chromium 多版本 | 相对规范 |
兼容策略上,我确定了一个原则:优先保证Chromium内核和Firefox的完整功能,对Trident内核做降级方案。所谓降级,不是完全不能用上传功能,而是如果不支持Blob.slice和FormData,就退回到一次性上传整个文件的方式(只适用于小于2GB的文件),并明确提示用户“当前浏览器不支持大文件断点续传,建议切换极速模式”。
3.2 关键兼容代码:slice方法适配
WebUploader源码里有一个Base.createReader方法,专门用于创建读分片的数据源。不同浏览器对Blob.slice的实现有历史差异:
- Chrome/Opera:
blob.slice(start, end) - Firefox:
blob.slice(start, end, contentType) - Safari/老Edge:
blob.slice(start, end),但早期有blob.webkitSlice和blob.mozSlice
我在改造时加了这样一段兼容适配:
javascript复制var sliceFn = (function() {
if (typeof File.prototype.slice === 'function') {
return File.prototype.slice;
} else if (typeof File.prototype.webkitSlice === 'function') {
return File.prototype.webkitSlice;
} else if (typeof File.prototype.mozSlice === 'function') {
return File.prototype.mozSlice;
}
return null;
})();
如果sliceFn为null,说明这个浏览器不支持分片读取。此时我在WebUploader初始化时不做分片处理,把整个文件作为单块上传,同时禁用了断点续传和暂停功能。
3.3 FormData构造与请求头发送
WebUploader原生的上传请求是通过xhr.send(binary)方式发送二进制流的。跨浏览器改造时,我统一改成用FormData构造multipart请求,这样兼容性最好。
javascript复制var formData = new FormData();
formData.append('file', chunkBlob, file.name);
formData.append('token', fileToken);
formData.append('chunk', currentChunk);
formData.append('chunks', totalChunks);
formData.append('name', file.name);
formData.append('size', file.size);
有两点需要注意:一是chunkBlob不能直接引用整个文件对象,必须是用slice切割出来的分片Blob,否则服务端收到的永远是整个文件;二是FormData.append的第三个参数filename如果不设置,某些服务端框架默认取blob,会导致文件名丢失,所以一定要显式传file.name。
另外,我在所有请求里加了自定义Header:X-Requested-With: XMLHttpRequest,用来让后端网关区分普通HTTP请求和AJAX分片请求,方便做超时控制和日志隔离。
3.4 不兼容场景的优雅降级
对于实在不兼容的浏览器,不能直接白屏崩溃。我在WebUploader初始化前先做一次能力检测:
javascript复制if (!window.File || !window.FileReader || !window.FormData || !sliceFn) {
// 提示用户使用现代浏览器,并切换到传统整包上传模式
uploader.options.chunked = false;
uploader.options.threads = 1;
}
降级模式下,上传变成最原始的“选文件 -> 读整个文件 -> POST到服务端”流程。对于小于2GB的视频文件,这个流程勉强可用;大于2GB的文件则会提示用户“当前浏览器不支持超大文件上传,请切换至极速模式或使用Chrome浏览器”。
这个处理思路很重要:跨浏览器兼容不等于每个浏览器都实现同样强大的功能,而是让每个环境都有可用的方案。真正核心的场景(超大文件、断点续传)做到好用,边缘场景做到不报错、有引导,这才是工程化的处理方式。
4. 实操过程:WebUploader改造实现
4.1 初始化WebUploader:关键参数配置
下面给出我改造后的完整初始化代码。去掉了大量无关配置,只保留核心部分:
javascript复制var uploader = WebUploader.Uploader({
// 上传按钮和区域
pick: {
id: '#picker',
multiple: true
},
dnd: '#dnd-area',
paste: '#dnd-area',
// 关键:使用H5模式,禁用Flash
runtimeOrder: 'html5',
// 分片配置
chunked: true,
chunkSize: 5 * 1024 * 1024, // 5MB
threads: 4, // 并发4个分片
// 是否发送文件名称等附加信息
formData: {},
// 服务端上传分片接口
server: '/api/upload/chunk',
// 自动上传:选完文件后手动触发,方便先检查断点状态
auto: false,
// 单个文件大小限制,这里设置50GB
fileSingleSizeLimit: 50 * 1024 * 1024 * 1024,
// 文件数量限制
fileNumLimit: 100,
// 压缩图片功能关闭
compress: false,
// 上传失败重试次数
retry: {
enable: true,
times: 3,
delay: 2000
}
});
有几个参数容易踩坑,我这里单独说明:
runtimeOrder: 'html5':必须显式指定。WebUploader默认会按html5,flash顺序启动运行时,如果客户端装过Flash,它会优先用Flash上传。Flash对大文件内存控制远不如H5,而且已经停止维护,所以直接禁用。compress: false:WebUploader默认对图片有压缩优化,如果不关掉,上传视频时可能触发对Blob的格式识别,导致大文件处理变慢。fileSingleSizeLimit:这个是WebUploader内置的校验,超过限制会触发错误回调,并不会真的把文件切开。所以分片逻辑还是要靠chunked: true。
4.2 断点续传核心:重写beforeFileQueued流程
我改造WebUploader的关键点是:在文件进入上传队列之前,先向后端查询该文件的断点信息。
javascript复制uploader.on('beforeFileQueued', function(file) {
// 先隐藏文件对象,等check完成后再决定是否加入队列
var deferred = WebUploader.Deferred();
// 构造文件标识
var token = calculateFileToken(file);
// 查询服务端断点信息
$.ajax({
url: '/api/upload/check',
method: 'GET',
data: {
token: token.token,
size: file.size,
name: file.name
},
dataType: 'json',
timeout: 10000,
success: function(res) {
if (res.exists) {
// 服务端已存在完整文件,直接跳过上传
uploader.skipFile(file);
deferred.resolve();
} else {
// 标记文件的分片状态
file.uploadToken = token.token;
file.uploadedChunks = res.uploadedChunks || [];
file.totalChunks = res.totalChunks || Math.ceil(file.size / 5 * 1024 * 1024);
// 从队列中删除已上传的分片
// 这步是WebUploader改造的难点,我们通过重写uploadFile内部逻辑来实现
uploader.addFile(file);
deferred.resolve();
}
},
error: function() {
// 网络异常时默认从零开始,但不能阻塞用户选文件
file.uploadToken = token.token;
file.uploadedChunks = [];
file.totalChunks = Math.ceil(file.size / 5 * 1024 * 1024);
uploader.addFile(file);
deferred.resolve();
}
});
// 返回Deferred,控制文件入队的时机
return deferred.promise();
});
这段代码解决了一个核心问题:先查断点状态,再决定怎么传。因为beforeFileQueued事件是同步的,如果不返回Deferred,文件会立即进入队列,这时候还没拿到服务端的断点信息,后续的分片过滤就无从谈起。
4.3 分片发送的核心逻辑:改造uploadFile
WebUploader的分片发送逻辑在uploadFile方法里。原版逻辑是拿到文件后,按文件大小切成N个分片,依次调用_sendChunk发送。改造后,需要让它在发送前先过滤掉已上传的分片。
我重写了uploader.option('uploadBeforeSend')来拦截每个分片的发送请求:
javascript复制uploader.option('uploadBeforeSend', function(block, data, headers) {
var file = block.file;
var chunkIndex = block.chunk;
// 核心过滤:如果这个分片已上传,直接跳过发送
if (file.uploadedChunks && file.uploadedChunks.indexOf(chunkIndex) >= 0) {
// 返回false告诉WebUploader这个分片不需要传
// 注意:上游会把这个分片标记为成功,从而跳过它
return false;
}
// 附加文件标识参数
data.token = file.uploadToken;
data.chunk = chunkIndex;
data.chunks = file.totalChunks;
data.name = file.name;
data.size = file.size;
return true;
});
这里有个重要的逻辑:uploadBeforeSend返回false时,WebUploader会认为当前分片“无需上传”,直接进入完成流程。通过这个机制,我们可以把已上传分片从发送队列中剔除,只上传缺失的分片。这就是断点续传在WebUploader里真正的落地位置。
不过我还发现一个缺陷:uploadBeforeSend返回false的分片,WebUploader会直接调用_onBlockComplete,导致该分片状态可能被标记为错误。经排查,需要再补一个事件,把“跳过”的分片统一标记为成功:
javascript复制uploader.on('uploadBlock', function(block) {
// 在真正发送前拦截
if (block.file.uploadedChunks && block.file.uploadedChunks.indexOf(block.chunk) >= 0) {
block.complete(); // 手动标记该分片完成
return false; // 阻止默认发送流程
}
});
4.4 暂停与恢复实现
暂停功能相对简单,WebUploader原生提供了stop方法。但要让暂停后在断点处续传,需要配合服务端的分片状态记录。
javascript复制// 暂停所有上传
$('#btn-pause').on('click', function() {
uploader.stop(true); // true表示暂停并保留文件队列
// 标记状态为已暂停
currentStatus = 'paused';
});
// 继续上传
$('#btn-resume').on('click', function() {
// 从服务端重新拉取已上传分片状态
$.each(uploader.getFiles('interrupt'), function(index, file) {
refreshFileChunkStatus(file);
});
uploader.upload();
});
refreshFileChunkStatus每次恢复时重新获取上传进度,确保前端内存里的已上传分片列表和服务端一致。
4.5 服务端接口设计参考
前端改造得再好,服务端不配合也是白搭。我按下面的接口规范去跟后端同事沟通,整体推进非常顺利。
接口1:检查文件状态
text复制GET /api/upload/check?token=abc&size=10485760&name=video.mp4
返回示例:
json复制{
"exists": false,
"uploadedChunks": [0,1,2,4],
"chunkSize": 5242880,
"totalChunks": 2000
}
接口2:上传分片
text复制POST /api/upload/chunk
Content-Type: multipart/form-data
参数:
- file: 二进制分片数据
- token: 文件标识
- chunk: 分片索引
- chunks: 总分片数
- name: 文件名
- size: 文件总大小
服务端存储路径:/data/tmp/{token}_{chunk}.part
返回:200 OK 或 409 分片已存在
接口3:合并分片
text复制POST /api/upload/merge
Body (JSON):
{
"token": "abc123",
"name": "video.mp4",
"size": 10485760,
"totalChunks": 2000
}
服务端合并逻辑:
python复制with open(final_path, 'wb') as outfile:
for i in range(total_chunks):
part_path = os.path.join(tmp_dir, f"{token}_{i}.part")
with open(part_path, 'rb') as infile:
shutil.copyfileobj(infile, outfile, 1024*1024)
os.remove(part_path)
注意合并时一定要用二进制模式打开文件,并且按索引顺序严格拼接。用文本模式或并行拼接都会导致视频文件损坏。
5. 常见问题与排查技巧实录
5.1 进度条走到99%就卡住不动了
这个问题我排查了很久。表面现象是进度条卡在99%,服务端看日志发现分片其实已经上传完毕,但前端迟迟没有收到合并请求。
原因在于WebUploader完成所有分片后,会触发一个uploadFinished事件,而我当时监听的是uploadSuccess事件。这两个事件的触发时机不同:
uploadSuccess:单个分片上传成功后触发uploadFinished:文件所有分片都上传成功后触发
正确的做法是在uploadFinished里调用合并接口。但还有一个坑:如果uploadFinished触发后,又有一个重试的分片被标记为成功,合并请求可能发送两次。解决方案是加一个锁:
javascript复制var isMerging = false;
uploader.on('uploadFinished', function(file) {
if (isMerging) return;
isMerging = true;
$.post('/api/upload/merge', {
token: file.uploadToken,
name: file.name,
size: file.size,
totalChunks: file.totalChunks
}).done(function() {
// 合并成功,标记文件为完成
}).fail(function() {
isMerging = false; // 合并失败要释放锁,允许重试
});
});
5.2 Chrome上能传,360浏览器兼容模式传不了
这个问题的根因是360兼容模式基于IE内核,不支持File.prototype.slice方法。前面提到的降级逻辑会自动把chunked关闭,但服务端接口如果只接受分片格式,就会导致上传失败。
最终的解决办法是服务端兼容两种上传模式:当请求不携带token、chunk等参数时,走传统整包上传接口;当请求携带分片参数时,走分片上传接口。类似网关路由的方式。前端在降级模式下发送的请求参数里加一个mode: 'file'标记,服务端根据mode字段分流到不同处理逻辑。
5.3 合并后的视频文件播放不了,大小对不上
这个问题的排查思路比较经典。我遇到过两种情况:
情况一:分片顺序错乱。 如果高并发时,服务端处理分片请求的时序不一致,某些分片可能先到达后一个索引,后到达前一个索引。合并时如果只是遍历total_chunks目录下的文件,按文件名排序,在数字大于10时排序会出错(例如0,1,10,2,3)。解决方法是文件名统一补零,例如{token}_0000.part、{token}_0001.part,或者合并前从数据库/接口明细里按索引顺序显式排列。
情况二:分片丢失。 某些分片上传时返回了200,但因为网络超时或服务端写盘延迟,实际上文件没写完整。解决方法是服务端在合并前检查所有分片文件的大小,正常情况下除了最后一个分片,每个分片大小应该等于约定分片大小(5MB)。如果哪个分片偏小,直接判定异常并跳过合并,要求前端重传该分片。
python复制expected_size = chunk_size
for i in range(total_chunks):
part_path = os.path.join(tmp_dir, f"{token}_{i}.part")
actual_size = os.path.getsize(part_path)
if i < total_chunks - 1 and actual_size != expected_size:
raise Exception(f"分片 {i} 大小异常: {actual_size} != {expected_size}")
5.4 上传过程中浏览器内存持续上涨,最终崩溃
这个问题的原因有两个:一是分片大小设置过大(比如我之前试过50MB分片);二是WebUploader默认会把文件的分片列表全部预生成到内存里,一个50GB的文件即便分成5MB,也有10000个分片对象,每个对象是带状态的,内存开销不小。
我的优化方案:
- 分片大小统一设为5MB,不盲目加大;
- 并发数控制在4以内;
- 引入
accept参数,限制文件类型为视频格式,避免一些非视频大文件进入队列; - 定时调用
uploader.refresh()释放无用DOM节点和事件绑定; - 在文件上传完成后,用
uploader.removeFile(file, true)立即从队列中移除该文件对象,释放内存占用。
5.5 跨域部署时上传失败
项目里存在前端静态资源域名和后端接口域名不同的情况。此时一定在前后端同时开启跨域支持:
服务端配置:
text复制Access-Control-Allow-Origin: http://your-frontend-domain
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, X-Requested-With
Access-Control-Allow-Methods: GET, POST, OPTIONS
前端XHR开启withCredentials:
javascript复制uploader.option('xhrWithCredentials', true);
同时,WebUploader内部对OPTIONS预检请求的处理比较特殊,如果服务端网关对OPTIONS不返回200,会在真正上传前卡住。排查时可以观察浏览器的Network面板,看是否有大量OPTIONS请求pending。
我的个人经验与后续可扩展方向
经过这一轮改造,我越来越觉得WebUploader这种老牌插件并没有过时,关键在于怎么理解它的底层机制。它把文件选择、队列管理、拖拽上传这些我们不愿意重复造的轮子打磨得足够成熟,我们只需要在它暴露的钩子上做二次开发。这次改造让我真正理解了“分片”不是把文件切成小块那么简单,分片大小、并发数、服务端配合、浏览器兼容性、失败重试策略,每一个环节都是环环相扣的。
最后分享一个自测技巧:在开发阶段,我经常用浏览器开发者工具的Network面板,把网络节流模式设为“Slow 3G”,然后不断刷新页面测试断点续传。这样能快速模拟极端弱网环境,发现很多在高速内网里暴露不出来的问题。这个场景下的前端调试,最重要的不是代码写得多花哨,而是“中断-恢复-再中断-再恢复”这套循环能稳定跑100次不丢数据。我在实际项目中踩过不少坑,这篇文章里的每个方案都是被线上问题逼出来的,希望对你有实际帮助。
如果后续要扩展,我建议可以做两个方向:一是接入文件秒传功能,在一开始用特征值判断服务端是否已有相同文件,如果有就直接跳过上传;二是把所有断点信息从前端内存搬到IndexedDB,这样即使浏览器整个崩溃重启,也能快速恢复上传状态,体验会比现在的刷新恢复再进一步。
