Dropzone.js 是前端文件上传领域非常流行的拖拽上传库,它提供了美观的预览界面和丰富的回调事件,但默认的上传模式是一次性把整个文件通过 FormData 发送给服务端。当文件体积超过几百兆甚至几个 GB 时,这种直传方式会遇到两个致命问题:一是浏览器内存占用飙升,二是网络一旦中断就必须从头再来。解决思路是把文件切成小块,每次只上传一小片,全部成功后再通知服务端合并,这就是分片上传。如果再配合记录已上传分片的序号,断网或刷新页面后可以跳过这些分片继续传,就实现了断点续传。

本文不打算从零实现一个分片上传组件,而是基于 Dropzone.js 已有的能力进行扩展。Dropzone 的 chunking 选项允许把文件按照指定大小分成多片,每次调用 processFile 时只发送当前片。关键点在于如何控制每一片的请求参数、如何显示分片级进度,以及中断后如何根据服务端返回的已上传分片列表恢复队列。
配置 Dropzone 的 chunking 参数并自定义分片请求
Dropzone 的分片功能并不是内置的完整方案,它只负责切割文件和触发多次上传回调,具体的请求格式需要我们自己通过 sending 和 params 等事件来定制。先创建一个基础 Dropzone 实例,把 chunking 设为 true,chunkSize 根据实际网络情况设为 2MB 到 10MB 之间。同时关闭自动处理,这样我们可以手动控制何时开始上传。
// 初始化 Dropzone,禁用自动上传
var myDropzone = new Dropzone("#uploader", {
url: "/api/upload/chunk",
method: "post",
chunking: true,
chunkSize: 2 * 1024 * 1024, // 每片 2MB
parallelChunkUploads: false, // 串行上传,避免服务端并发压力
autoProcessQueue: false,
maxFilesize: 10240, // 允许最大 10GB
clickable: true
});
这里设置了 parallelChunkUploads: false,让分片按顺序逐个发送。串行上传的好处是服务端可以简单按索引顺序追加写入,不用处理乱序合并的问题。如果要提高上传速度,可以改成 true,但后端需要支持并发写入时的文件锁或临时目录管理,否则文件内容可能错乱。
当用户选择文件后,Dropzone 会把文件加入队列,每个文件会有一个 upload 对象,其中 chunks 数组保存了所有分片信息。我们需要在 sending 事件中为每个分片添加额外参数,比如当前分片索引、总片数、文件唯一标识等。文件唯一标识通常由前端生成,可以用文件名加大小加修改时间拼一个 hash,或者由后端在初始化接口中返回一个 uploadId。
myDropzone.on("sending", function(file, xhr, formData) {
var upload = file.upload;
var chunkIndex = upload.chunkIndex; // 当前分片索引,从0开始
var totalChunks = upload.chunks.length;
formData.append("chunkIndex", chunkIndex);
formData.append("totalChunks", totalChunks);
formData.append("fileId", file.customId); // 自定义的唯一标识
formData.append("filename", file.name);
});
注意 file.upload.chunkIndex 只在分片上传过程中有效,每发送一片就会递增。发送完成后 Dropzone 会触发 success 或 error 事件,我们可以利用这一点在每片成功时更新进度条,并且检查是否所有分片都已上传完毕,然后调用合并接口。
实现断点续传:恢复未完成的上传队列
断点续传的核心是服务端能够返回某个文件已经接收了哪些分片。理想的做法是提供一个状态查询接口,前端在页面加载或用户重新选择同一文件时先调用这个接口,拿到已上传分片索引数组,然后把 Dropzone 队列中对应的分片标记为已完成,跳过它们。
Dropzone 本身没有直接支持跳过已上传分片的 API,但我们可以通过修改 file.upload.chunks 数组来达到目的。每个 chunk 对象上有一个 processed 属性,当它被设为 true 时,Dropzone 的 processChunk 方法会跳过该分片。更简单的做法是重新构造一个只包含未上传分片的新文件上传流程,也就是把原文件重新切成相同大小的片,但只把未上传的索引加入队列。这里选择用 Dropzone 的 accept 回调配合自定义状态判断。
// 模拟恢复:假设 fileId 对应的已上传分片索引为 [0,1,2]
var uploadedChunks = [0, 1, 2];
myDropzone.on("addedfile", function(file) {
file.customId = generateFileId(file); // 根据文件属性生成唯一ID
// 这里先不自动处理,等用户点击开始按钮
});
function startUpload() {
// 先查询服务端已上传分片
$.get("/api/upload/status", { fileId: currentFile.customId }, function(res) {
var uploaded = res.uploadedChunks || [];
// 修改 file.upload.chunks 中对应分片的 processed 状态
// 但 Dropzone 在 autoProcessQueue 为 false 时,upload 对象可能还没生成
// 需要先手动处理队列
if (currentFile.upload === null) {
myDropzone.processFile(currentFile);
}
// 处理完成后,upload 对象才会有 chunks
var upload = currentFile.upload;
for (var i = 0; i < upload.chunks.length; i++) {
if (uploaded.indexOf(i) !== -1) {
upload.chunks[i].processed = true;
}
}
// 然后再触发上传处理流程
myDropzone.processQueue();
});
}
上面的代码片段展示了一个思路,但实际执行顺序需要仔细调整。因为 Dropzone 在调用 processFile 之前不会创建 upload 对象。更稳妥的办法是在 sending 事件中进行判断:如果当前分片索引已经在服务端返回的已上传列表中,就终止这个分片的请求,并手动触发 success。但这样做会导致 Dropzone 认为整个文件上传完成,需要额外处理。
一个更干净的方案是不使用 Dropzone 的内部分片上传,而是自己控制每个分片的 XMLHttpRequest。Dropzone 只负责文件选择和拖拽交互,上传逻辑完全由我们写。这样断点续传的粒度控制最灵活,但会丢失 Dropzone 的进度条自动更新。折中方案是继续使用 Dropzone 的 chunking,但在 addedfile 之后立即调用 processFile,然后立刻在 sending 事件中根据已上传索引跳过请求。如果跳过请求,需要模拟 xhr 的 complete 流程,否则 Dropzone 会卡住。
为了简化演示,下面给出一个基于自定义请求的断点续传核心逻辑。通过维护一个 uploadedSet 数组记录已上传分片,在每片成功回调中把当前索引加入集合,进度条的计算根据集合大小除以总片数来更新。
var uploadedSet = new Set(); // 已上传分片索引
var totalChunks = 0;
var currentFile = null;
var chunkSize = 2 * 1024 * 1024;
function startChunkUpload(file) {
currentFile = file;
totalChunks = Math.ceil(file.size / chunkSize);
// 先查询已上传分片,填充 uploadedSet
$.get("/api/upload/status", { fileId: file.customId }, function(res) {
res.uploadedChunks.forEach(function(idx) {
uploadedSet.add(idx);
});
// 从第一个未上传的分片开始
uploadNextChunk(0);
});
}
function uploadNextChunk(index) {
if (index >= totalChunks) {
// 全部分片上传完成,调用合并接口
$.post("/api/upload/merge", { fileId: currentFile.customId, totalChunks: totalChunks })
.done(function() {
alert("上传完成");
uploadedSet.clear();
});
return;
}
if (uploadedSet.has(index)) {
// 该分片已上传,跳过
uploadNextChunk(index + 1);
return;
}
var start = index * chunkSize;
var end = Math.min(start + chunkSize, currentFile.size);
var blob = currentFile.slice(start, end);
var formData = new FormData();
formData.append("chunk", blob);
formData.append("chunkIndex", index);
formData.append("fileId", currentFile.customId);
$.ajax({
url: "/api/upload/chunk",
type: "POST",
data: formData,
processData: false,
contentType: false,
xhr: function() {
var xhr = $.ajaxSettings.xhr();
if (xhr.upload) {
xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
var percent = (uploadedSet.size + (e.loaded / e.total)) / totalChunks * 100;
$("#progress").text(percent.toFixed(1) + "%");
}
}, false);
}
return xhr;
},
success: function() {
uploadedSet.add(index);
uploadNextChunk(index + 1);
},
error: function() {
// 网络错误,停止上传,保留 uploadedSet 供下次恢复
console.log("上传中断,已上传分片数:" + uploadedSet.size);
}
});
}
这段代码演示了手动控制分片上传的完整流程。每个分片通过 file.slice 切割,用 FormData 发送。进度计算逻辑是已上传整片数量加上当前片已传输比例,再除以总片数。中断时不做任何清理,下次调用 startChunkUpload 时重新查询服务端状态,uploadedSet 会被重新填充,自然跳过已上传的分片。
后端接口约定与常见错误排查
分片上传离不开服务端的配合。前端把文件切成小块后,后端需要提供三个接口:分片上传接口、状态查询接口、合并接口。分片上传接口接收 chunk 文件块、chunkIndex 索引、fileId 文件标识,将数据追加写入对应临时文件或者保存为独立分片文件。状态查询接口根据 fileId 返回已接收分片索引列表。合并接口在所有分片上传完成后被调用,按照索引顺序把分片合并成最终文件,然后删除临时数据。
实际开发中最容易踩的一个坑是 chunkSize 设置过小导致请求次数过多。比如一个 5GB 的文件,如果用 1MB 每片,需要发送 5120 个请求,每个请求都有 HTTP 头和 TCP 握手开销,整体时间反而更长。推荐分片大小在 2MB 到 5MB 之间,根据用户平均带宽调整。另一个常见问题是后端限制了单个请求体大小,比如 Nginx 默认 client_max_body_size 是 1MB,如果分片超过这个值会收到 413 错误,需要修改配置或减小分片。
还有一点是关于文件唯一标识的生成。如果只用文件名加大小,同名不同内容的文件会发生冲突。更可靠的方式是用 SparkMD5 或 Web Crypto API 计算文件 hash,但计算大文件的完整 hash 非常耗时,会阻塞 UI。折中方案是计算前几个分片的 hash 加上文件大小和修改时间,或者由服务端在初始化时生成一个 UUID 返回给前端,前端始终携带这个 UUID 进行上传和查询。
并发控制方面,如果每个分片请求都独立鉴权,可以在请求头中携带 token 或 cookie。服务端需要防止恶意用户跳过部分分片直接调用合并接口,应该在合并前校验所有分片是否齐全,并做文件大小和 hash 校验。另外合并操作应该异步执行,避免阻塞 HTTP 请求线程,可以放入消息队列处理,前端轮询合并状态。
Dropzone.js分片上传断点续传修改时间:2026-09-21 05:41:27