🎯 1. 概述
在大文件上传场景中,传统的上传方式存在诸多问题,如:
- 文件过大,上传超时或失败:一次性上传大文件容易受到网络环境和带宽限制的影响,导致上传失败。
- 上传中断需重新上传:如果上传过程中网络中断或其他原因导致上传失败,必须重新上传整个文件,浪费了大量的时间和带宽。
- 无法复用已上传数据:对于重复上传的文件,服务器无法识别已经存在的数据,用户需重新上传,无法提高上传效率。
为了解决上述问题,通常使用以下几种技术手段:
- 切片上传(Chunk Upload) :将大文件拆分为多个小块,逐块上传,可以有效应对大文件上传失败的问题,减少上传失败带来的影响。
- 断点续传(Resumable Upload) :记录上传进度,上传失败后从中断处继续上传,避免重复上传已经成功的部分。
- 秒传(Instant Upload) :利用文件的唯一标识(如
hash
),检测文件是否已上传,若文件已存在,直接跳过上传,节省时间和带宽。 - 暂停/恢复上传:在上传过程中可以随时暂停上传任务,并在恢复时继续上传,极大提升用户的操作体验。
这些技术手段相互结合,可以显著提升大文件上传的稳定性、效率和用户体验,特别适用于大文件、网络环境不稳定的场景。
📚 2. 实现思路
2.1 流程概述
大文件上传的核心流程通常包括以下几个步骤:
-
选择文件,计算文件
hash
- 通过
File
对象读取文件并计算其唯一的hash
值,作为文件的唯一标识。
- 通过
-
切片处理,生成文件分片
- 将文件按照固定的大小(如 5MB)切分为若干小块,方便并行上传,减轻服务器压力。
-
上传前检查(校验秒传与已上传切片)
- 在上传文件前向服务器发送文件
hash
,检查是否已有上传记录,若存在已上传分片则跳过,支持秒传和断点续传。
- 在上传文件前向服务器发送文件
-
上传分片,更新进度,支持暂停与恢复
- 依次上传切片,支持并行上传和暂停/恢复,实时更新上传进度,提升用户操作体验。
-
合并分片,完成上传
- 所有切片上传完成后,通知服务器进行文件合并,生成完整文件,确保数据一致性。
通过上述流程,可以有效提高上传效率,避免重复上传,增强用户操作的灵活性。
📐 3. 关键技术与实现
3.1 文件切片
切片是实现大文件上传的基础,通过 File.prototype.slice
方法可以轻松将文件切割为若干小块,避免单次上传文件过大导致的失败。
以下代码实现了一个文件切片的函数,支持自定义切片大小(默认为 5MB)。
// 文件切片方法
function createFileChunks(file: File, chunkSize: number = 5 * 1024 * 1024) {
const chunks = [];
let cur = 0;
// 循环遍历文件,按照指定的 chunkSize 进行切片
while (cur < file.size) {
chunks.push(file.slice(cur, cur + chunkSize)); // 通过 slice 方法切割文件
cur += chunkSize; // 移动当前游标
}
return chunks; // 返回切片数组
}
3.2 生成文件 hash
生成 hash
是实现秒传和断点续传的关键,常用方法包括使用 Crypto
API 或第三方库计算文件内容的 MD5/SHA-256 值。
以下代码使用 spark-md5
库对文件进行分片计算,生成文件的唯一 hash
值。
import SparkMD5 from 'spark-md5';
// 生成文件 hash
function calculateFileHash(chunks: Blob[]): Promise<string> {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
let index = 0;
// 递归读取每个分片并计算 hash
const loadNext = () => {
if (index < chunks.length) {
const reader = new FileReader();
reader.onload = (e) => {
spark.append(e.target?.result as ArrayBuffer); // 追加每个分片的数据
index++;
loadNext(); // 继续处理下一个分片
};
reader.readAsArrayBuffer(chunks[index]); // 以 ArrayBuffer 形式读取分片数据
} else {
resolve(spark.end()); // 所有分片处理完成,返回最终 hash 值
}
};
loadNext();
});
}
3.3 上传前校验
上传前,前端应向服务器发送 hash
请求,服务器检查是否存在已上传的切片列表。
此操作可以实现秒传和断点续传,避免重复上传已存在的数据。
// 上传前校验文件是否已上传
async function checkFile(hash: string) {
return await fetch('/api/upload/check', {
method: 'POST',
body: JSON.stringify({ hash }),
}).then((res) => res.json()); // 返回服务器的校验结果
}
3.4 上传的主流程
// 上传主流程
async function handleUpload(file) {
const chunks = createFileChunks(file);
const hash = await calculateFileHash(chunks);
const { uploadedChunks } = await checkFile(hash);
for (let i = 0; i < chunks.length; i++) {
if (!uploadedChunks.includes(i)) {
await uploadChunk(hash, i, chunks[i]);
}
}
await mergeChunks(hash, chunks.length);
alert('上传完成');
}
3.4 Node.js 服务端实现
使用 Node.js 处理切片上传、断点续传、秒传和合并切片。
const fs = require('fs');
const path = require('path');
// 检查文件是否已上传
app.post('/api/upload/check', (req, res) => {
const { hash } = req.body;
const uploadedChunks = getUploadedChunks(hash);
res.json({ uploadedChunks });
});
// 处理切片上传
app.post('/api/upload/chunk', (req, res) => {
const { hash, index } = req.body;
const chunkPath = path.join(__dirname, 'uploads', `${hash}-${index}`);
req.pipe(fs.createWriteStream(chunkPath));
req.on('end', () => res.status(200).send('ok'));
});
// 合并切片
app.post('/api/upload/merge', (req, res) => {
const { hash, total } = req.body;
const filePath = path.join(__dirname, 'uploads', hash);
for (let i = 0; i < total; i++) {
fs.appendFileSync(filePath, fs.readFileSync(`${filePath}-${i}`));
fs.unlinkSync(`${filePath}-${i}`);
}
res.status(200).send('merge completed');
});
📊 4. 总结
通过切片上传、断点续传、秒传与暂停/恢复上传技术的结合,可以显著提升大文件上传的效率与可靠性。这种方案特别适用于网络环境复杂或大文件传输需求强烈的场景,既能提升用户体验,又能有效节省带宽和时间。