本文目录导读:

将一个大文件分成多个小块,客户端记录已经成功上传的分块,下次续传时只上传缺失的分块,最后服务端将所有分块合并成完整文件。
下面从核心原理、实现步骤、代码示例以及关键注意事项四个方面来详细说明。
核心原理
- 分片:客户端将大文件按固定大小(如 1MB、5MB)切割成多个小块。
- 记录进度:客户端记录已成功上传的分片索引或分片的哈希值。
- 查询已上传部分:续传前,客户端向服务端询问“这个文件我已经传了哪些分片?”。
- 增量上传:客户端只上传缺失或上传失败的分片。
- 合并:所有分片上传完成后,服务端按顺序合并成原文件。
实现步骤(前后端协作)
前端(浏览器/客户端)负责:
- 文件分片
- 使用 HTML5 API:
file.slice(start, end)切割文件。 - 每个分片分配一个唯一的索引号(
chunkIndex)。
- 使用 HTML5 API:
- 生成文件唯一标识
- 对文件整体计算一个 MD5 或 SHA-1 哈希(或者用
File.name + File.size + lastModified组合)。
- 对文件整体计算一个 MD5 或 SHA-1 哈希(或者用
- 上传前查询
- 请求服务端接口:
GET /upload/status?fileHash=xxx。 - 服务端返回已收到的分片索引列表。
- 请求服务端接口:
- 并发/顺序上传缺失分片
- 使用
FormData发送分片数据,同时携带fileHash和chunkIndex。
- 使用
- 处理异常
如果某个分片上传失败,记录索引,稍后重试。
- 通知合并
- 所有分片上传完毕后,请求服务端:
POST /upload/merge?fileHash=xxx。
- 所有分片上传完毕后,请求服务端:
后端(服务端)负责:
- 接收分片
- 创建临时目录:
/tmp/uploads/{fileHash}/。 - 将收到的分片写入临时文件,
part、part。
- 创建临时目录:
- 记录已接收的分片
- 可以用数据库、Redis 或直接通过文件系统检测(
os.listdir列出目录下的 .part 文件)。
- 可以用数据库、Redis 或直接通过文件系统检测(
- 返回已接收分片列表
当客户端查询时,返回目录中已有的分片索引。
- 合并文件
merge接口:按索引顺序读取所有.part文件,二进制追加写入目标文件。- 合并完成后,清理临时目录。
代码示例(简化版 Node.js + 前端)
前端代码(JavaScript)
// 1. 文件分片
const file = document.getElementById('fileInput').files[0];
const CHUNK_SIZE = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileHash = await calculateFileHash(file); // 用 SparkMD5 等库计算
// 2. 查询已上传分片
const response = await fetch(`/upload/status?fileHash=${fileHash}`);
const uploadedChunks = await response.json(); // 返回已上传的分片索引数组,如 [0,1,2,5]
// 3. 上传缺失分片
for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) continue; // 已上传的跳过
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('fileHash', fileHash);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);
// 带重试逻辑的上传
await uploadWithRetry('/upload/chunk', formData, 3);
}
// 4. 通知合并
await fetch(`/upload/merge?fileHash=${fileHash}&fileName=${file.name}`, { method: 'POST' });
后端代码(Node.js + Express 简化版)
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const uploadDir = '/tmp/uploads/';
// 分片上传接口
app.post('/upload/chunk', multer().single('file'), (req, res) => {
const { fileHash, chunkIndex } = req.body;
const chunkDir = path.join(uploadDir, fileHash);
if (!fs.existsSync(chunkDir)) fs.mkdirSync(chunkDir, { recursive: true });
const chunkPath = path.join(chunkDir, `${chunkIndex}.part`);
fs.writeFileSync(chunkPath, req.file.buffer);
res.send({ success: true });
});
// 查询已上传分片
app.get('/upload/status', (req, res) => {
const { fileHash } = req.query;
const chunkDir = path.join(uploadDir, fileHash);
if (!fs.existsSync(chunkDir)) return res.json([]);
const chunks = fs.readdirSync(chunkDir).map(f => parseInt(f.replace('.part', '')));
res.json(chunks);
});
// 合并文件
app.post('/upload/merge', (req, res) => {
const { fileHash, fileName } = req.query;
const chunkDir = path.join(uploadDir, fileHash);
const totalChunks = fs.readdirSync(chunkDir).length;
const finalPath = path.join(uploadDir, fileName);
const writeStream = fs.createWriteStream(finalPath);
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(chunkDir, `${i}.part`);
const data = fs.readFileSync(chunkPath);
writeStream.write(data);
}
writeStream.end();
// 清理临时分片
fs.rmSync(chunkDir, { recursive: true });
res.send({ success: true, path: finalPath });
});
关键注意事项与优化
| 关注点 | 说明与建议 |
|---|---|
| 文件哈希(完整性校验) | 计算文件整体哈希开销大,可改为计算每个分片的 MD5,服务端收到后校验,防止数据损坏。 |
| 并发控制 | 前端可以并发上传 3-5 个分片(提升速度),但需要处理分片乱序到达的问题(服务端靠索引顺序合并,本身不依赖到达顺序)。 |
| 断网恢复 | 使用 localStorage 或 IndexedDB 记录已上传的分片索引,浏览器刷新或断网后,重新计算哈希并查询服务端状态。 |
| 服务端存储策略 | - 小规模:直接用文件系统存储 .part 文件。- 大规模:用 Redis 记录分片状态,用对象存储(如 S3、阿里云OSS)直接上传分片(它们原生支持分片上传 API)。 |
| 过期清理 | 未在 24 小时内完成的临时分片应清理(可用定时任务检查文件修改时间)。 |
| 秒传 | 在查询上传状态时,如果服务端发现文件哈希已存在且完整,直接返回“上传成功”,无需重复上传。 |
总结流程
用户选择文件
↓
前端计算 fileHash + 分片
↓
前端询问服务端:fileHash 已有哪些分片?
↓
服务端返回已收分片列表
↓
前端跳过已有分片,只上传缺失分片
↓
所有分片上传完毕 → 请求合并
↓
服务端按顺序合并分片 → 返回最终文件
断点续传的本质就是 “记录进度 + 部分重传”,实际项目中,可以直接使用成熟方案(如阿里云 OSS 分片上传、Resumable.js、Uppy.js),也可以自己实现以满足定制化需求。