在上一篇文章中,我们探讨了如何在浏览器端高效计算大文件的 MD5 值。有了这个基础,我们就可以进一步实现一个完整的大文件分片上传(Chunked Upload)方案。
分片上传不仅能解决大文件上传超时的问题,还能实现断点续传和秒传等高级功能,极大地提升用户体验。
整体流程设计
一个健壮的分片上传流程通常包含以下三个阶段:
- 握手(Handshake)/ 预检查:前端发送文件的 MD5 和基本信息给后端。后端检查文件是否已存在(秒传),或者是否存在部分分片(断点续传),并返回给前端需要上传的分片列表。
- 分片上传(Chunk Upload):前端根据后端返回的列表,将文件切割成对应的分片,并发上传。
- 合并(Merge):所有分片上传完成后,前端发送合并请求,后端将所有分片合并成原始文件。
前端实现
1. 文件切片
利用 File.prototype.slice 方法,我们可以轻松地将文件切割成多个 Blob 对象。
1 2 3 4 5 6 7 8 9
| const createFileChunks = (file, chunkSize = 2 * 1024 * 1024) => { const chunks = []; let cur = 0; while (cur < file.size) { chunks.push({ index: cur / chunkSize, file: file.slice(cur, cur + chunkSize) }); cur += chunkSize; } return chunks; };
|
2. 并发控制
为了避免一次性发出成百上千个请求导致浏览器崩溃或服务器过载,我们需要控制并发请求的数量。
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
| const uploadChunks = async (chunks, uploadedList = []) => { const requestList = chunks .filter(({ index }) => !uploadedList.includes(index)) .map(({ file, index }) => { const formData = new FormData(); formData.append("file", file); formData.append("hash", fileHash); formData.append("index", index); return { formData, index }; });
const asyncPool = async (limit, array, iteratorFn) => { const ret = []; const executing = []; for (const item of array) { const p = Promise.resolve().then(() => iteratorFn(item, array)); ret.push(p);
if (limit <= array.length) { const e = p.then(() => executing.splice(executing.indexOf(e), 1)); executing.push(e); if (executing.length >= limit) { await Promise.race(executing); } } } return Promise.all(ret); };
await asyncPool(3, requestList, ({ formData }) => axios.post("/upload", formData)); };
|
后端实现 (Node.js 示例)
1. 接收分片
后端接收到分片后,将其临时存储在一个以文件 MD5 命名的目录中。
1 2 3 4 5 6 7 8 9 10 11
| const fse = require("fs-extra"); const multipart = require("connect-multiparty");
app.post("/upload", multipart(), async (req, res) => { const { index, hash } = req.body; const chunkPath = path.resolve(UPLOAD_DIR, hash, index); await fse.move(req.files.file.path, chunkPath); res.send({ msg: "切片上传成功" }); });
|
2. 合并分片
当所有分片上传完毕,后端读取该目录下的所有分片,按索引顺序写入到一个新文件中,最后删除临时目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const mergeFileChunks = async (filePath, fileHash, size) => { const chunkDir = path.resolve(UPLOAD_DIR, fileHash); const chunkPaths = await fse.readdir(chunkDir); chunkPaths.sort((a, b) => a - b); await Promise.all( chunkPaths.map((chunkPath, index) => pipeStream( path.resolve(chunkDir, chunkPath), fse.createWriteStream(filePath, { start: index * size, end: (index + 1) * size }) ) ) ); fse.rmdirSync(chunkDir); };
|
核心功能点解析
秒传 (Instant Upload)
在“握手”阶段,如果后端发现数据库中已经存在该 MD5 对应的文件,则直接返回“上传成功”,前端无需发送任何数据。这就是“秒传”的原理。
断点续传 (Resumable Upload)
如果上传过程中断,下次上传时,前端先询问后端已存在哪些分片。后端扫描临时目录,返回已上传的分片索引列表(例如 [0, 1, 2])。前端在 filter 阶段就会跳过这些分片,只上传剩下的部分。
总结
大文件分片上传是一个经典的前后端协作场景。前端负责高效计算 MD5、切片和并发控制;后端负责分片存储、合并以及状态管理。通过这种方案,我们可以构建出稳定、高效且体验优秀的文件上传服务。