前言
在大文件上传场景中,传统的一次性上传方式会面临网络不稳定、内存占用高、服务器压力大等问题。本文将介绍如何通过分片上传、断点续传和秒传技术优化用户体验,并附上完整代码实现。
一、核心功能与需求分析
1. 分片上传
- 目标:将大文件切割为多个小分片,分批次上传。
- 优势:降低单次请求压力,提升上传稳定性。
2. 断点续传
- 目标:上传中断后,可从断点处继续上传。
- 实现:记录已上传的分片信息,跳过已传分片。
3. 秒传
- 目标:服务器已存在相同文件时,直接返回结果,无需重复上传。
- 实现:通过文件哈希(如MD5)验证文件唯一性。
二、实现原理
1. 分片上传流程
1. 用户选择文件 → 2. 计算文件哈希 → 3. 文件分片 → 4. 上传分片 → 5. 合并分片
2. 技术依赖
- 前端:
File API、SparkMD5(计算哈希)、Axios(HTTP请求) - 后端:分片存储、合并接口、哈希校验接口
三、代码实现
1. HTML结构
<input type="file" id="fileInput" />
<button onclick="upload()">开始上传</button>
<div id="progress"></div>
2. 计算文件哈希(秒传关键)
import SparkMD5 from 'spark-md5';
// 计算文件哈希(Web Worker优化)
async function calculateHash(file) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
const chunkSize = 2 * 1024 * 1024; // 2MB分片计算哈希
let currentChunk = 0;
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < Math.ceil(file.size / chunkSize)) {
loadNext();
} else {
resolve(spark.end());
}
};
function loadNext() {
const start = currentChunk * chunkSize;
const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
reader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
3. 分片上传逻辑
async function upload() {
const file = document.getElementById('fileInput').files[0];
if (!file) return;
// 1. 计算文件哈希(用于秒传)
const fileHash = await calculateHash(file);
// 2. 检查是否已存在文件(秒传)
const { data: existData } = await axios.post('/api/check', { fileHash });
if (existData.exist) {
alert('秒传成功!');
return;
}
// 3. 分片上传
const chunkSize = 5 * 1024 * 1024; // 5MB分片
const chunkCount = Math.ceil(file.size / chunkSize);
const uploadedChunks = existData.uploadedChunks || [];
for (let i = 0; i < chunkCount; i++) {
// 跳过已上传的分片(断点续传)
if (uploadedChunks.includes(i)) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('hash', fileHash);
formData.append('index', i);
await axios.post('/api/upload', formData, {
onUploadProgress: (e) => {
const progress = ((i * chunkSize + e.loaded) / file.size * 100).toFixed(2);
document.getElementById('progress').innerText = `上传进度:${progress}%`;
},
});
}
// 4. 通知合并分片
await axios.post('/api/merge', { fileHash, fileName: file.name });
alert('上传成功!');
}
四、后端关键接口设计(Node.js示例)
1. 检查文件是否已存在(秒传)
app.post('/api/check', (req, res) => {
const { fileHash } = req.body;
const filePath = path.resolve(__dirname, 'uploads', `${fileHash}.merge`);
// 检查文件是否存在
if (fs.existsSync(filePath)) {
return res.json({ exist: true });
}
// 检查已上传的分片(断点续传)
const chunkDir = path.resolve(__dirname, 'uploads', fileHash);
const uploadedChunks = fs.existsSync(chunkDir)
? fs.readdirSync(chunkDir).map(Number)
: [];
res.json({ exist: false, uploadedChunks });
});
2. 分片上传接口
app.post('/api/upload', (req, res) => {
const { chunk, hash, index } = req.files;
const chunkDir = path.resolve(__dirname, 'uploads', hash);
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir);
}
// 保存分片
fs.renameSync(chunk.path, path.resolve(chunkDir, index));
res.json({ success: true });
});
3. 合并分片接口
app.post('/api/merge', async (req, res) => {
const { fileHash, fileName } = req.body;
const chunkDir = path.resolve(__dirname, 'uploads', fileHash);
const chunks = fs.readdirSync(chunkDir);
// 按分片索引排序
chunks.sort((a, b) => a - b);
// 合并文件
const writeStream = fs.createWriteStream(
path.resolve(__dirname, 'uploads', `${fileHash}.merge`)
);
for (const chunk of chunks) {
const chunkPath = path.resolve(chunkDir, chunk);
const buffer = fs.readFileSync(chunkPath);
writeStream.write(buffer);
fs.unlinkSync(chunkPath); // 删除分片
}
writeStream.end();
res.json({ success: true });
});
五、优化点
- Web Worker计算哈希:避免阻塞主线程。
- 并发上传控制:限制同时上传的分片数,减轻浏览器压力。
- 错误重试机制:对失败分片自动重试。
- 进度条优化:使用更友好的UI组件展示进度。