在实现文件上传时,通常会去讨论文件分片上传、秒传以及断点续传等问题,这里整理一下解决方法.
问题 | 分片上传的好处 |
---|---|
大文件上传超时 / 失败 | 分片失败可重传,避免全部重传 |
网络不稳定 | 分片可以断点续传 |
并发能力弱 | 分片可并发上传提高速度 |
浏览器限制(如 body 大小) | 避免单次请求过大被限制 |
支持上传进度显示 | 每片进度可追踪 |
文件分片上传是一种将大文件分割成多个小片段(分片),然后逐个上传这些分片的技术。这种方法有几个优点,包括提高上传的成功率、减少内存占用以及支持断点续传等。下面简要介绍文件分片上传的概念和实现方法。
前端流程:
- 选择文件并读取其大小
- 将文件按固定大小(如 1MB)切片(使用
Blob.slice
) - 逐片上传(可并发)
- 上传成功后通知服务器进行合并
可以通过hash分片验证是否已经上传,从而提升用户文件上传体验.
Hash检测 实现秒传
秒传的关键是“文件去重” —— 客户端通过计算文件指纹(如 MD5、SHA256),先向服务器查询是否已存在相同文件,如果存在则跳过上传,直接“秒传成功”。
实现步骤:
- 前端计算文件 hash(通常用
CryptoJS
、spark-md5
)。 - 发请求询问服务器:这个 hash 是否已上传?
- 如果服务器返回“存在”:
- 返回文件地址(或秒传成功)
- 否则再走正常上传流程。
注意事项:
- 前端文件 hash 计算是异步的,推荐使用
Web Worker
加速。 - 服务端需要维护文件 hash 与实际文件路径的映射表(如 Redis、数据库)
1 | const file = ref<File | null>(null); |
切片上传
上传表单form/multipart
数据,包括切片索引以及hash信息,后端每次下载切片到一个文件夹下.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55const uploadChunks = async (chunks: Blob[]) => {
const data = chunks.map((chunk: Blob, index: number) => {
const formData = new FormData();
formData.append("fileHash", fileHash.value as string);
formData.append("chunk", chunk);
formData.append("size", chunk.size.toString());
formData.append("chunkIndex", index.toString());
formData.append("chunkHash", fileHash.value + "-" + index);
return formData;
});
console.log(data);
const MAX_REQUEST = 6;
// 并发请求
const taskPool: Promise<Response>[] = [];
let index = 0;
while (index < data.length) {
const task = fetch("http://localhost:3000/upload", {
method: "POST",
body: data[index],
});
taskPool.push(task);
task
.then((response) => {
if (response.ok) {
console.log("分片上传成功");
taskPool.splice(taskPool.indexOf(task), 1);
} else {
console.error("分片上传失败");
}
})
.catch((error) => {
console.error("Error uploading chunk:", error);
});
if (taskPool.length == MAX_REQUEST) {
Promise.race(taskPool)
.then((result) => {
if (result.ok) {
console.log("分片上传成功");
taskPool.splice(taskPool.indexOf(task), 1);
} else {
console.error("分片上传失败");
}
// 只要有一个完成就返回
// 如果上传成功,移除该任务
})
.catch((error) => {
console.error("Error uploading chunk:", error);
});
}
index++;
}
await Promise.all(taskPool);
};
原理:
大文件切成多个小块(如每块 1MB),逐个上传,上传完毕后由服务器合并为完整文件。
实现步骤:
前端将文件切片,通常通过:
1
const chunk = file.slice(start, end);
每个切片附带索引和文件唯一标识(如 hash)上传。
后端接收每个切片,按 hash 和索引编号保存。
前端上传完所有切片后,请求服务端合并文件。
关键点:
- 客户端:控制并发上传(推荐并发数 3~5)。
- 服务端:需要记录哪些切片已经上传(可用 Redis 或文件夹索引)。
- 最后合并文件时可用
fs.appendFile
或fs.createWriteStream
文件合并
1 | const mergeRequest = async () => { |
1 | app.post("/merge", async (req, res) => { |
在切片上传完毕之后再发送merge请求.
断点续传
1 | // 客户端 |
1 | // 服务端 |
原理:
上传中断后,可以只上传未完成的部分,而不用从头开始。
实现方式:
- 通常与分片上传结合使用。
- 客户端在重试上传前,先向服务端询问已上传的切片列表。
- 客户端只上传未完成的切片,最后合并。
关键点:
- 客户端需记录上传状态(如切片状态、索引)。
- 服务端提供接口返回已有的切片(如
/upload/status?fileHash=xxx
)。 - 文件 hash 一定要稳定,用于唯一标识该文件上传任务。
服务端解决方案
技术点 | 实现方式 |
---|---|
存储切片 | 保存到本地临时目录 or 对象存储(如 OSS、S3) |
存储状态 | Redis(高效) or MongoDB/MySQL |
文件合并 | fs.createWriteStream + fs.createReadStream |
断点续传状态 | 文件夹结构 + index.json、数据库记录 |
推荐前端工具库
Web Workers 是一种让网页内容在后台线程中运行脚本的方式。工作线程可以在不影响用户界面的情况下执行任务。此外,它们可以使用 fetch()
或 XMLHttpRequest
API 发起网络请求。一旦创建,一个工作线程可以通过向创建它的 JavaScript 代码指定的事件处理器发送消息来与其通信(反之亦然)
Using Web Workers - Web APIs | MDN1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const fileInput = document.getElementById('fileInput');
const result = document.getElementById('result');
// 创建 Worker 实例
const worker = new Worker('hash-worker.js');
worker.onmessage = function (event) {
result.textContent = '文件 MD5: ' + event.data;
};
fileInput.addEventListener('change', function () {
const file = this.files[0];
if (!file) return;
// 读取文件为 ArrayBuffer,发送给 Worker
const reader = new FileReader();
reader.onload = function () {
worker.postMessage(reader.result); // ArrayBuffer 发送给 Worker
};
reader.readAsArrayBuffer(file);
});
WorkerGlobalScope: importScripts() method - Web APIs | MDN1
2
3
4
5
6
7
8
9
10
11// 引入 spark-md5(注意路径)或使用 CDN + importScripts
importScripts('https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js');
onmessage = function (event) {
const arrayBuffer = event.data;
const spark = new self.SparkMD5.ArrayBuffer();
spark.append(arrayBuffer);
const hash = spark.end();
postMessage(hash);
};
- 主线程不卡顿。
- 计算任务可异步处理,提升用户体验。
ArrayBuffer
是可转移对象,性能更好(不会拷贝,只是转移)。
注意事项
- Worker 中无法访问 DOM、
window
。 - Worker 是异步的,不支持
alert
、confirm
等。 - 跨域引用
worker.js
时需要注意同源策略,或使用 Blob 方式创建 Worker。
实战优化
支持大文件(如 1GB+),首选分片上传 + 秒传。
支持上传失败重试(单个切片失败可重试)。
使用服务端合并标记(如
done.flag
)避免重复合并。上传前先校验文件 hash,实现秒传。
提供进度回调、UI反馈。