本文目录导读:

我来为你详细讲解文件分片上传、合并和断点续传的实现方案。
整体架构
客户端(浏览器) 服务器
| |
|---- 1. 初始化上传请求 ------>|
|<--- 返回 uploadId ---------|
| |
|---- 2. 分片上传(chunk) ---->|
|---- 3. 上传进度反馈 -------->|
|---- 4. 断点续传查询 -------->|
|<--- 返回已上传分片列表 ----|
| |
|---- 5. 合并分片请求 -------->|
|<--- 返回完整文件URL ------|
前端实现
1 核心上传类
class FileUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 2 * 1024 * 1024; // 默认2MB
this.parallelLimit = options.parallelLimit || 3; // 并发数
this.retryCount = options.retryCount || 3; // 重试次数
this.baseUrl = options.baseUrl || '/api/upload';
this.pendingQueue = [];
this.activeCount = 0;
this.fileHash = null;
this.uploadId = null;
}
// 计算文件哈希
async calculateHash(file) {
const chunkSize = 1024 * 1024; // 1MB
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
for (let i = 0; i < chunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
const reader = new FileReader();
await new Promise((resolve) => {
reader.onload = (e) => {
spark.append(e.target.result);
this.updateProgress({
type: 'hash',
loaded: i + 1,
total: chunks
});
resolve();
};
reader.readAsArrayBuffer(blob);
});
}
return spark.end();
}
// 初始化上传
async initUpload(file) {
this.fileHash = await this.calculateHash(file);
const response = await axios.post(`${this.baseUrl}/init`, {
fileName: file.name,
fileSize: file.size,
fileHash: this.fileHash,
chunkSize: this.chunkSize
});
this.uploadId = response.data.uploadId;
return response.data;
}
// 切片文件
sliceFile(file) {
const chunks = [];
let start = 0;
let index = 0;
while (start < file.size) {
const end = Math.min(start + this.chunkSize, file.size);
const blob = file.slice(start, end);
chunks.push({
blob,
index,
start,
end,
size: blob.size
});
start = end;
index++;
}
return chunks;
}
// 上传单个分片
async uploadChunk(chunk, retryCount = 0) {
const formData = new FormData();
formData.append('chunk', chunk.blob);
formData.append('index', chunk.index);
formData.append('uploadId', this.uploadId);
formData.append('fileHash', this.fileHash);
try {
const response = await axios.post(`${this.baseUrl}/chunk`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
if (this.onChunkProgress) {
this.onChunkProgress(chunk.index, progressEvent);
}
}
});
return response.data;
} catch (error) {
if (retryCount < this.retryCount) {
// 指数退避重试
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retryCount)));
return this.uploadChunk(chunk, retryCount + 1);
}
throw error;
}
}
// 获取已上传分片(断点续传)
async getUploadedChunks() {
try {
const response = await axios.get(`${this.baseUrl}/chunks`, {
params: {
uploadId: this.uploadId,
fileHash: this.fileHash
}
});
return response.data.uploadedChunks || [];
} catch (error) {
return [];
}
}
// 控制并发上传
async uploadWithConcurrency(chunks, uploadedChunks = []) {
const uploadPromises = [];
const uploadedIndices = new Set(uploadedChunks);
//过滤已上传的分片
const remainingChunks = chunks.filter(chunk => !uploadedIndices.has(chunk.index));
for (let i = 0; i < this.parallelLimit && i < remainingChunks.length; i++) {
uploadPromises.push(this.processNextChunk(remainingChunks));
}
let completedChunks = uploadedIndices.size;
const totalChunks = chunks.length;
while (uploadPromises.length > 0) {
const completedPromise = await Promise.race(uploadPromises);
completedChunks++;
// 更新进度
if (this.onProgress) {
this.onProgress({
loaded: completedChunks,
total: totalChunks,
percent: (completedChunks / totalChunks) * 100
});
}
// 移除已完成的任务
const index = uploadPromises.indexOf(completedPromise);
uploadPromises.splice(index, 1);
// 添加新的任务
if (remainingChunks.length > 0) {
uploadPromises.push(this.processNextChunk(remainingChunks));
}
}
return completedChunks;
}
// 处理下一个分片
async processNextChunk(remainingChunks) {
const chunk = remainingChunks.shift();
await this.uploadChunk(chunk);
return chunk.index;
}
// 合并分片
async mergeChunks() {
const response = await axios.post(`${this.baseUrl}/merge`, {
uploadId: this.uploadId,
fileHash: this.fileHash,
fileName: this.file.name,
totalChunks: Math.ceil(this.file.size / this.chunkSize)
});
return response.data;
}
// 主上传方法
async upload(file) {
this.file = file;
try {
// 1. 初始化上传
const initData = await this.initUpload(file);
console.log('Upload initialized:', initData);
// 2. 切片
const chunks = this.sliceFile(file);
// 3. 检查已上传的分片(断点续传)
const uploadedChunks = await this.getUploadedChunks();
if (uploadedChunks.length > 0) {
console.log(`Resuming upload, ${uploadedChunks.length} chunks already uploaded`);
}
// 4. 上传分片
await this.uploadWithConcurrency(chunks, uploadedChunks);
// 5. 合并分片
const result = await this.mergeChunks();
console.log('Upload completed:', result);
return result;
} catch (error) {
console.error('Upload failed:', error);
throw error;
}
}
}
2 使用示例
// HTML部分
/*
<div id="upload-container">
<input type="file" id="fileInput" />
<div id="progress-bar">
<div id="progress-fill"></div>
</div>
<span id="progress-text">0%</span>
<button id="uploadBtn">上传</button>
<button id="pauseBtn">暂停</button>
</div>
*/
// JavaScript部分
const uploader = new FileUploader({
chunkSize: 1024 * 1024, // 1MB
parallelLimit: 3,
retryCount: 3
});
// 设置进度回调
uploader.onProgress = (progress) => {
document.getElementById('progress-fill').style.width = `${progress.percent}%`;
document.getElementById('progress-text').textContent = `${Math.round(progress.percent)}%`;
};
// 暂停功能
let isPaused = false;
document.getElementById('pauseBtn').addEventListener('click', () => {
isPaused = !isPaused;
if (isPaused) {
// 取消当前的请求
axios.CancelToken.source().cancel('Upload paused');
}
});
// 上传按钮
document.getElementById('uploadBtn').addEventListener('click', async () => {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('请选择文件');
return;
}
try {
const result = await uploader.upload(file);
alert('上传成功!');
} catch (error) {
if (axios.isCancel(error)) {
console.log('Upload paused');
} else {
alert('上传失败:' + error.message);
}
}
});
后端实现(Node.js + Express)
// server.js
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// 配置存储
const UPLOAD_DIR = path.join(__dirname, 'uploads');
const TEMP_DIR = path.join(__dirname, 'temp');
// 确保目录存在
if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true });
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
// 存储上传任务信息
const uploadTasks = new Map();
// 配置 multer
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const chunkDir = path.join(TEMP_DIR, req.body.fileHash);
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir, { recursive: true });
}
cb(null, chunkDir);
},
filename: (req, file, cb) => {
cb(null, `${req.body.index}`);
}
});
const upload = multer({ storage });
// 初始化上传
app.post('/api/upload/init', (req, res) => {
const { fileName, fileSize, fileHash, chunkSize } = req.body;
// 生成上传ID
const uploadId = crypto.randomUUID();
// 计算总分片数
const totalChunks = Math.ceil(fileSize / chunkSize);
// 保存任务信息
uploadTasks.set(uploadId, {
fileName,
fileSize,
fileHash,
chunkSize,
totalChunks,
uploadedChunks: [],
createdAt: Date.now()
});
res.json({
uploadId,
totalChunks,
chunkSize
});
});
// 上传分片
app.post('/api/upload/chunk', upload.single('chunk'), (req, res) => {
const { uploadId, index, fileHash } = req.body;
// 更新任务信息
const task = uploadTasks.get(uploadId);
if (task) {
if (!task.uploadedChunks.includes(parseInt(index))) {
task.uploadedChunks.push(parseInt(index));
}
}
res.json({
code: 0,
message: 'Chunk uploaded successfully'
});
});
// 获取已上传的分片
app.get('/api/upload/chunks', (req, res) => {
const { uploadId, fileHash } = req.query;
const task = uploadTasks.get(uploadId);
let uploadedChunks = [];
if (task) {
uploadedChunks = task.uploadedChunks;
} else {
// 检查文件是否已存在(秒传)
const filePath = path.join(UPLOAD_DIR, fileHash);
if (fs.existsSync(filePath)) {
uploadedChunks = 'all'; // 文件已存在
}
}
res.json({
uploadedChunks
});
});
// 合并分片
app.post('/api/upload/merge', async (req, res) => {
const { uploadId, fileHash, fileName, totalChunks } = req.body;
const task = uploadTasks.get(uploadId);
if (!task) {
return res.status(404).json({ error: 'Upload task not found' });
}
const chunkDir = path.join(TEMP_DIR, fileHash);
const filePath = path.join(UPLOAD_DIR, fileHash);
try {
// 创建写入流
const writeStream = fs.createWriteStream(filePath);
// 按顺序合并分片
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(chunkDir, `${i}`);
// 检查分片是否存在
if (!fs.existsSync(chunkPath)) {
writeStream.destroy();
fs.unlinkSync(filePath);
throw new Error(`Chunk ${i} not found`);
}
// 读取并写入分片
const chunkBuffer = fs.readFileSync(chunkPath);
writeStream.write(chunkBuffer);
}
// 完成写入
writeStream.end();
// 等待写入完成
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
// 清理临时文件
fs.rmSync(chunkDir, { recursive: true, force: true });
// 清理任务信息
uploadTasks.delete(uploadId);
res.json({
code: 0,
message: 'File merged successfully',
data: {
url: `/files/${fileHash}`,
fileName: fileName
}
});
} catch (error) {
res.status(500).json({
code: -1,
message: error.message
});
}
});
// 文件访问
app.get('/files/:hash', (req, res) => {
const filePath = path.join(UPLOAD_DIR, req.params.hash);
if (fs.existsSync(filePath)) {
res.sendFile(filePath);
} else {
res.status(404).json({ error: 'File not found' });
}
});
// 定期清理过期任务
setInterval(() => {
const now = Date.now();
const maxAge = 24 * 60 * 60 * 1000; // 24小时
for (const [uploadId, task] of uploadTasks) {
if (now - task.createdAt > maxAge) {
// 清理临时文件
const chunkDir = path.join(TEMP_DIR, task.fileHash);
if (fs.existsSync(chunkDir)) {
fs.rmSync(chunkDir, { recursive: true, force: true });
}
uploadTasks.delete(uploadId);
}
}
}, 60 * 60 * 1000); // 每小时检查一次
app.listen(3000, () => {
console.log('Server running on port 3000');
});
关键特性说明
1 断点续传实现
- 客户端先查询已上传分片
- 跳过已上传的分片
- 只上传未完成的分片
2 并发控制
- 限制同时上传的分片数量
- 使用 Promise.race 实现并发控制
- 避免过多并发导致服务器压力
3 错误重试
- 失败自动重试
- 使用指数退避策略
- 设置最大重试次数
4 秒传功能
- 通过文件哈希检查
- 如果文件已存在,直接返回结果
- 节省带宽和时间
注意事项
- 安全考虑:添加文件类型验证、大小限制
- 内存管理:使用流式处理大文件
- 进度反馈:实时显示上传进度
- 错误处理:完善错误处理机制
- 清理机制:定期清理过期任务和临时文件
这个方案可以处理大文件上传,支持断点续传,适合在实际生产环境中使用。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播