一、核心流程设计
-
分片上传
- 前端分片切割:使用
File.slice()将文件按固定大小(如 1MB)切割为多个 Blob 分片。 - 生成唯一标识:通过 SparkMD5 等库计算文件内容的哈希值,作为文件唯一 ID。
- 并发控制:通过
Promise.all或队列管理控制同时上传的分片数(如 3-5 个并发)。
- 前端分片切割:使用
-
断点续传
- 记录上传状态:前端通过 LocalStorage 或 IndexedDB 保存文件哈希与已上传分片索引。
- 服务端校验:上传前先查询服务端,获取已上传分片列表,跳过已传部分。
-
分片合并
- 服务端按序合并:收到合并请求后,按分片索引顺序将临时分片合并为完整文件。
- 清理临时文件:合并完成后删除临时分片,释放存储空间。
二、前端实现关键代码
// 计算文件哈希(Web Worker 优化)
async function calculateHash(file) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = (e) => {
spark.append(e.target.result);
resolve(spark.end());
};
});
}
// 分片上传逻辑
async function uploadFile(file) {
const chunkSize = 1 * 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
const fileHash = await calculateHash(file);
// 查询已上传分片
const { uploaded } = await axios.get(`/api/check?hash=${fileHash}`);
// 创建分片上传任务
const tasks = [];
for (let i = 0; i < totalChunks; i++) {
if (uploaded.includes(i)) continue;
const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('hash', fileHash);
formData.append('index', i);
tasks.push(axios.post('/api/upload', formData));
}
// 并发控制(例如每次上传3个分片)
while (tasks.length > 0) {
const batch = tasks.splice(0, 3);
await Promise.all(batch);
updateProgress(); // 更新进度条
}
// 合并请求
await axios.post(`/api/merge?hash=${fileHash}&name=${file.name}`);
}
三、服务端实现要点
-
API 设计
GET /api/check:根据文件哈希返回已上传分片列表。POST /api/upload:接收分片并保存到临时目录。POST /api/merge:合并所有分片并保存最终文件。
-
分片存储
// Node.js 示例(使用 Express) const UPLOAD_DIR = path.resolve(__dirname, 'temp'); app.post('/api/upload', (req, res) => { const { chunk, hash, index } = req.files; const chunkDir = path.resolve(UPLOAD_DIR, hash); if (!fs.existsSync(chunkDir)) fs.mkdirSync(chunkDir); fs.renameSync(chunk.path, path.resolve(chunkDir, index)); // 保存分片 res.send({ code: 0 }); }); -
分片合并
app.post('/api/merge', async (req, res) => { const { hash, name } = req.query; const chunkDir = path.resolve(UPLOAD_DIR, hash); const chunks = fs.readdirSync(chunkDir); // 按索引排序后合并 chunks.sort((a, b) => a - b).forEach(chunk => { fs.appendFileSync(path.resolve(UPLOAD_DIR, name), fs.readFileSync(path.resolve(chunkDir, chunk))); }); // 清理临时文件 fs.rmdirSync(chunkDir, { recursive: true }); res.send({ code: 0 }); });
四、优化与异常处理
-
哈希计算优化
- 抽样哈希:对大文件取头尾和中间部分计算哈希,减少耗时。
- Web Worker:防止主线程阻塞。
-
断点续传增强
- 服务端记录分片哈希,避免分片被篡改。
- 客户端异常退出后,重新选择文件时自动恢复进度。
-
错误重试机制
- 分片上传失败时自动重试(如最多 3 次)。
- 网络中断后提示用户手动恢复上传。
五、安全性考虑
- 身份验证:上传接口需校验用户权限(如 JWT)。
- 分片校验:服务端校验分片 MD5,防止数据损坏。
- 防重复上传:相同哈希文件直接返回现有地址。