上传几百兆甚至几个 G 的视频、安装包时,普通的整文件上传体验很差:网络一抖就要从头再来,弱网下几乎传不完,进度条也无法准确反映状态。生产环境的大文件上传普遍采用"分片上传":客户端把文件切成固定大小的块并行上传,服务端合并,并在此基础上实现断点续传、秒传和失败重试。本文按客户端切分、服务端接收、合并、秒传判定、并发与可靠性这条主线,给出一套完整实现。
一次分片上传的完整生命周期是:
关键点是:上传状态以服务端记录为准,客户端随时可以根据"已收到分片列表"跳过已传部分,这就是断点续传的基础。
用文件内容 hash(如基于 web worker 算 SHA-256,或先用抽样+大小+修改时间做快速指纹)作为唯一标识。先调用预检接口:
async function precheck(fileHash, fileSize) {
const res = await fetch("/upload/precheck", {
method: "POST",
body: JSON.stringify({ fileHash, fileSize })
});
return res.json(); // { uploaded:true }秒传 | { uploaded:false, chunks:[2,5]已存在 }
}服务端若发现该 hash 的完整文件已存在,直接返回已有地址实现"秒传";若只传了一部分,返回已存在的分片序号,客户端跳过这些分片。大文件全量 hash 较慢,可以先用"文件大小 + 首尾若干字节 + 抽样块"算快速指纹做初判,合并时再做完整校验兜底。
function sliceFile(file, chunkSize = 5 * 1024 * 1024) {
const chunks = [];
for (let i = 0; i < file.size; i += chunkSize) {
chunks.push({ index: chunks.length, blob: file.slice(i, i + chunkSize) });
}
return chunks;
}
async function uploadChunks(fileHash, chunks, doneSet, concurrency = 3) {
const queue = chunks.filter(c => !doneSet.has(c.index));
const workers = Array.from({ length: concurrency }, worker);
let cursor = 0;
async function worker() {
while (cursor < queue.length) {
const chunk = queue[cursor++];
const fd = new FormData();
fd.append("fileHash", fileHash);
fd.append("index", chunk.index);
fd.append("chunk", chunk.blob);
await retry(() => fetch("/upload/chunk", { method: "POST", body: fd }), 3);
}
}
await Promise.all(workers);
}并发数要克制:并发太高会挤占带宽、加重服务端压力,一般 3~6 个为宜;每个分片要带独立重试,单片失败不应导致整个任务失败。
服务端把每个分片暂存到以文件 hash 命名的目录,文件名用序号保证顺序:
app.post("/upload/chunk", async (ctx) => {
const { fileHash, index } = ctx.request.body;
const buf = ctx.request.files.chunk;
const dir = path.join(TMP_DIR, fileHash);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path.join(dir, `${index}.part`), buf.data); // 幂等覆盖写
const uploaded = await fs.readdir(dir);
ctx.body = { ok: true, uploaded: uploaded.map(f => Number(f.split(".")[0])) };
});合并时严格按序号读取、用流拼接,避免一次性把大文件读进内存:
async function mergeChunks(fileHash, total, finalName) {
const dest = fs.createWriteStream(path.join(FILE_DIR, finalName));
for (let i = 0; i < total; i++) {
const part = path.join(TMP_DIR, fileHash, `${i}.part`);
if (!fs.existsSync(part)) throw new Error(`missing chunk ${i}`);
await pipeline(fs.createReadStream(part), dest, { end: false });
}
dest.end();
}合并后校验整体大小与总 hash,确认无缺失、无损坏,再清理临时目录。
断点续传不需要特殊机制,靠的就是"服务端记录 + 上传前预检":页面刷新、网络中断后,重新用文件指纹查询已传分片,跳过即可。要让它真正可靠,需要注意:
落地时建议固定分片大小(5~10MB 是较平衡的选择,过小会增加请求数,过大则单片重试成本高),服务端用对象存储的分片上传能力承接最终文件、本地只做短暂中转或直传签名,避免应用服务器成为带宽和存储瓶颈。客户端把 hash 计算放进 Web Worker 防止阻塞主线程,大文件提供"快速指纹初判 + 合并时完整校验"兼顾速度与正确性。还要处理好多端场景:同一账号在不同设备续传时,进度查询要基于文件指纹而不是设备本地。最后用弱网模拟、中途断网、杀进程重进、并发重试等手段反复验证,确认任意时刻中断都能无损恢复。
分片上传的价值不只是"能传大文件",更是把一次脆弱的长传输变成了许多次可靠、可重试、可恢复的小传输。抓住"文件指纹定身份、服务端记录定进度、流式合并保内存、完整校验保正确"这几点,断点续传、秒传和并发上传就能在同一套机制里自然成立。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。