本文目录导读:

核心原理
文件分片上传将大文件分割成多个小片段,分别上传,然后在服务端合并。
前端实现
1 基础分片上传
class FileUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 1 * 1024 * 1024; // 默认1MB
this.threads = options.threads || 3; // 并发数
this.file = null;
this.chunks = [];
this.uploadedChunks = new Set();
}
// 分片处理
createChunks(file) {
this.file = file;
this.chunks = [];
let start = 0;
let index = 0;
while (start < file.size) {
const end = Math.min(start + this.chunkSize, file.size);
const chunk = file.slice(start, end);
this.chunks.push({
file: chunk,
index: index,
start: start,
end: end,
size: chunk.size,
blob: chunk,
name: `${file.name}.part.${index}`,
hash: this.generateHash(chunk, index)
});
start = end;
index++;
}
}
// 生成分片哈希(用于验证)
generateHash(chunk, index) {
return `${this.file.name}_${index}_${chunk.size}_${Date.now()}`;
}
// 上传单个分片
async uploadChunk(chunk) {
const formData = new FormData();
formData.append('file', chunk.file);
formData.append('name', chunk.name);
formData.append('totalChunks', this.chunks.length);
formData.append('chunkIndex', chunk.index);
formData.append('fileName', this.file.name);
formData.append('fileSize', this.file.size);
formData.append('hash', chunk.hash);
try {
const response = await fetch('/upload/chunk', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
this.uploadedChunks.add(chunk.index);
this.updateProgress();
return true;
}
return false;
} catch (error) {
console.error(`Chunk ${chunk.index} upload failed:`, error);
return false;
}
}
// 并发上传所有分片
async upload() {
const uploadPromises = [];
const chunks = [...this.chunks];
// 限制并发数
const queue = [];
while (chunks.length > 0) {
const chunk = chunks.shift();
const promise = this.uploadChunk(chunk).finally(() => {
queue.splice(queue.indexOf(promise), 1);
});
queue.push(promise);
if (queue.length >= this.threads) {
await Promise.race(queue);
}
}
// 等待所有上传完成
await Promise.all(queue);
// 通知服务器合并文件
return this.mergeFile();
}
// 通知服务器合并文件
async mergeFile() {
const response = await fetch('/upload/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: this.file.name,
totalChunks: this.chunks.length,
fileSize: this.file.size
})
});
return response.json();
}
// 更新进度
updateProgress() {
const progress = (this.uploadedChunks.size / this.chunks.length) * 100;
this.onProgress && this.onProgress(progress);
}
// 断点续传(检查已上传的分片)
async checkUploadedChunks(fileName) {
const response = await fetch(`/upload/status?fileName=${fileName}`);
const data = await response.json();
if (data.exists) {
data.uploadedChunks.forEach(chunk => {
this.uploadedChunks.add(chunk);
});
}
}
}
// 使用示例
const uploader = new FileUploader({
chunkSize: 2 * 1024 * 1024, // 2MB
threads: 5
});
uploader.onProgress = (progress) => {
console.log(`上传进度: ${progress.toFixed(2)}%`);
};
// 监听文件选择
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
// 检查已上传的分片(断点续传)
await uploader.checkUploadedChunks(file.name);
// 创建分片并上传
uploader.createChunks(file);
const result = await uploader.upload();
console.log('上传结果:', result);
});
2 带进度条和暂停恢复功能
class AdvancedFileUploader {
constructor(options) {
this.chunkSize = options.chunkSize || 2 * 1024 * 1024;
this.maxRetries = options.maxRetries || 3;
this.paused = false;
this.aborted = false;
}
// 上传带重试机制
async uploadChunkWithRetry(chunk, retryCount = 0) {
if (this.paused || this.aborted) {
return false;
}
try {
const result = await this.uploadChunk(chunk);
return result;
} catch (error) {
if (retryCount < this.maxRetries && !this.aborted) {
await this.delay(Math.pow(2, retryCount) * 1000); // 指数退避
return this.uploadChunkWithRetry(chunk, retryCount + 1);
}
throw error;
}
}
// 暂停上传
pause() {
this.paused = true;
}
// 恢复上传
resume() {
this.paused = false;
this.upload();
}
// 取消上传
abort() {
this.aborted = true;
// 通知服务器取消
fetch('/upload/abort', {
method: 'POST',
body: JSON.stringify({ fileName: this.file.name })
});
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
后端实现 (Node.js)
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs').promises;
const app = express();
const upload = multer({ dest: 'uploads/' });
// 存储分片
app.post('/upload/chunk', upload.single('file'), async (req, res) => {
try {
const { fileName, chunkIndex, totalChunks, hash } = req.body;
const chunkDir = path.join('uploads', fileName);
// 创建临时目录
await fs.mkdir(chunkDir, { recursive: true });
// 移动分片到指定目录
const chunkPath = path.join(chunkDir, `chunk_${chunkIndex}`);
await fs.rename(req.file.path, chunkPath);
res.json({ success: true, message: 'Chunk uploaded' });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 合并分片
app.post('/upload/merge', async (req, res) => {
try {
const { fileName, totalChunks, fileSize } = req.body;
const chunkDir = path.join('uploads', fileName);
const finalPath = path.join('uploads', 'completed', fileName);
// 确保目标目录存在
await fs.mkdir(path.dirname(finalPath), { recursive: true });
// 合并所有分片
const writeStream = fs.createWriteStream(finalPath);
for (let i = 0; i < totalChunks; i++) {
const chunkPath = path.join(chunkDir, `chunk_${i}`);
const chunkData = await fs.readFile(chunkPath);
writeStream.write(chunkData);
}
writeStream.end();
// 等待写入完成
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
// 验证文件大小
const stats = await fs.stat(finalPath);
if (stats.size !== parseInt(fileSize)) {
throw new Error('File size mismatch');
}
// 清理临时分片
await fs.rm(chunkDir, { recursive: true, force: true });
res.json({
success: true,
message: 'File merged successfully',
path: finalPath
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 检查上传状态(断点续传)
app.get('/upload/status', async (req, res) => {
try {
const { fileName } = req.query;
const chunkDir = path.join('uploads', fileName);
try {
const files = await fs.readdir(chunkDir);
const uploadedChunks = files
.filter(f => f.startsWith('chunk_'))
.map(f => parseInt(f.split('_')[1]));
res.json({
exists: true,
uploadedChunks
});
} catch (error) {
res.json({ exists: false, uploadedChunks: [] });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 取消上传
app.post('/upload/abort', async (req, res) => {
try {
const { fileName } = req.body;
const chunkDir = path.join('uploads', fileName);
// 清理临时文件
await fs.rm(chunkDir, { recursive: true, force: true });
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
使用示例
<!DOCTYPE html>
<html>
<head>
<style>
.upload-container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
}
.progress-bar {
width: 100%;
height: 20px;
background: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
transition: width 0.3s ease;
}
.controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-danger {
background: #f44336;
color: white;
}
</style>
</head>
<body>
<div class="upload-container">
<h2>文件分片上传</h2>
<input type="file" id="fileInput" />
<div class="progress-bar">
<div class="progress" id="progress" style="width: 0%"></div>
</div>
<div id="status">准备上传...</div>
<div class="controls">
<button class="btn btn-primary" onclick="startUpload()">开始上传</button>
<button class="btn btn-warning" onclick="pauseUpload()">暂停</button>
<button class="btn btn-primary" onclick="resumeUpload()">继续</button>
<button class="btn btn-danger" onclick="cancelUpload()">取消</button>
</div>
</div>
<script>
let uploader = null;
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
uploader = new FileUploader({
chunkSize: 1 * 1024 * 1024, // 1MB
threads: 3
});
uploader.onProgress = (progress) => {
document.getElementById('progress').style.width = progress + '%';
document.getElementById('status').textContent =
`上传进度: ${progress.toFixed(2)}%`;
};
document.getElementById('status').textContent = '准备上传...';
});
async function startUpload() {
if (!uploader) return;
const file = document.getElementById('fileInput').files[0];
uploader.createChunks(file);
document.getElementById('status').textContent = '开始上传...';
const result = await uploader.upload();
if (result.success) {
document.getElementById('status').textContent = '上传完成!';
}
}
function pauseUpload() {
if (uploader && uploader.pause) {
uploader.pause();
document.getElementById('status').textContent = '已暂停';
}
}
function resumeUpload() {
if (uploader && uploader.resume) {
uploader.resume();
document.getElementById('status').textContent = '继续上传...';
}
}
function cancelUpload() {
if (uploader && uploader.abort) {
uploader.abort();
document.getElementById('status').textContent = '已取消';
document.getElementById('progress').style.width = '0%';
}
}
</script>
</body>
</html>
优化建议
1 性能优化
- 动态调整分片大小:根据网络状况调整
- 并发控制:合理设置并发数
- 缓存机制:缓存已上传的分片
2 错误处理
- 重试机制:失败自动重试
- 数据校验:MD5/SHA256校验
- 日志记录:详细错误日志
3 安全性
- 文件类型验证:检查MIME类型
- 大小限制:防止恶意超大文件
- 访问控制:身份验证
这个实现支持大文件断点续传、并发上传、进度显示等功能,可以根据实际需求调整参数。