核心在于分片上传和断点续传
-
分片上传 (Chunked Upload)
将文件按固定大小(如 2MB 或 5MB)切割成多个 Blob 分片,为每个分片生成唯一标识,并发或顺序上传
流程:
前端计算文件唯一标识(如 MD5/SHA-1,可使用 spark-md5 库),用于服务端判断是否为同一文件。
将文件分片,记录总分片数、当前分片索引。
为每个分片单独发起 POST 或 PUT 请求上传。
服务端接收分片并暂存(如磁盘或 OSS)。
全部分片上传完毕后,前端通知服务端进行合并。 -
断点续传 (Resumable Upload)
原理: 在上传过程中记录上传进度,即使中断(刷新页面、网络断开),再次上传时可以从已上传的部分继续,而无需重新上传全部。
实现:
前端: 使用 localStorage 或 IndexedDB 持久化存储已成功上传的分片索引列表。续传时,先向服务器查询该文件已上传的分片列表(秒传和续传判断),然后只上传剩余分片。
服务端: 需要提供两个接口:
verify 接口: 接收文件哈希值,返回已上传的分片索引列表。
merge 接口: 在所有分片上传完成后,触发合并操作。 -
并发控制
同时发起大量 HTTP 请求可能会阻塞浏览器。需要限制并发数(如使用 p-limit、async-pool 等库),维护一个请求队列。
具体实现步骤
- 文件选择与读取
const file = document.querySelector('input[type=file]').files[0];
- 生成文件哈希(用于标识文件)
import SparkMD5 from 'spark-md5';
async function calculateFileHash(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);
const hash = spark.end();
resolve(hash);
};
});
}
const fileHash = await calculateFileHash(file);
- 文件分片
const chunkSize = 5 * 1024 * 1024; // 5MB
const chunkList = [];
let start = 0;
while (start < file.size) {
const chunk = file.slice(start, start + chunkSize);
chunkList.push({
chunk,
hash: fileHash + '_' + start, // 分片唯一标识
index: start / chunkSize,
});
start += chunkSize;
}
- 验证与获取已上传列表
const { data } = await axios.post('/api/verify', {
fileHash,
fileName: file.name,
chunkSize,
});
const uploadedList = data.uploadedList || []; // 服务端返回的已上传分片hash列表
- 上传分片(含并发控制)
const maxConcurrent = 3; // 最大并发数
const uploadChunks = async (chunks, uploadedList) => {
const pool = [];
const tasks = chunks.filter(chunk => !uploadedList.includes(chunk.hash));
for (let i = 0; i < tasks.length; i++) {
const formData = new FormData();
formData.append('chunk', tasks[i].chunk);
formData.append('hash', tasks[i].hash);
formData.append('fileHash', fileHash);
formData.append('index', tasks[i].index);
const task = axios.post('/api/upload', formData, {
onUploadProgress: (e) => {
// 更新该分片及总进度条
}
}).then(() => {
// 上传成功,可从池中移除
pool.splice(pool.indexOf(task), 1);
});
pool.push(task);
if (pool.length >= maxConcurrent) {
await Promise.race(pool); // 等待池中任意一个任务完成
}
}
await Promise.all(pool); // 等待所有剩余任务完成
};
- 通知合并
await axios.post('/api/merge', {
fileHash,
fileName: file.name,
chunkSize,
totalChunks: chunkList.length,
});
console.log('上传并合并成功!');
服务端(Node.js 示例)关键职责
/api/verify:
根据 fileHash 检查目标文件是否已存在(实现秒传)。
检查暂存目录,返回已上传的分片标识列表。
/api/upload:
接收分片文件,以 fileHash 为名创建临时目录,以分片 hash 或 index 为名保存分片。
/api/merge:
根据 fileHash 找到临时目录,按 index 顺序读取所有分片,通过文件流(fs.createWriteStream)合并成最终文件。
合并后删除临时分片。
简单流程
- 用户选择文件 → 前端计算 hash。
- 调用 /check 接口,返回已上传分片列表。
- 将未上传分片并发上传至 /upload-chunk。
- 全部分片上传后,调用 /merge 合并。
- 返回最终文件 URL。
进阶优化
Web Workers: 将计算文件哈希的耗时操作放在 Worker 线程,避免页面卡顿。
HTTP/2: 利用其多路复用特性,提升并发上传效率。
第三方库
simple-uploader.js(基于 Flow.js)
resumable.js
vue-simple-uploader(Vue 组件)
评论区