分片上传
分片上传是 WebUploader Plus 的核心功能之一,它将大文件切分成多个小块并发上传,极大提高了大文件上传的成功率和速度。
什么是分片上传
分片上传(Chunk Upload)是指将一个大文件切分成多个小块(chunk),然后分别上传这些小块,服务器端接收到所有分块后再合并成完整文件。
优势
- 突破大小限制:绕过服务器单文件大小限制
- 提高成功率:小块上传失败后可以单独重试
- 断点续传:网络中断后可以从断点处继续
- 并发上传:多个分块可以并发上传,提高速度
- 进度可控:更精确的上传进度反馈
基本配置
开启分片上传
javascript
var uploader = WebUploader.create({
pick: '#filePicker',
server: '/upload',
// 开启分片上传
chunked: true,
// 分片大小(5MB)
chunkSize: 5 * 1024 * 1024,
// 分片失败后重试次数
chunkRetry: 3,
// 分片失败重试间隔(毫秒)
chunkRetryDelay: 1000,
// 并发上传数
threads: 3
});配置说明
- chunked: 是否开启分片上传,默认
false - chunkSize: 每个分片的大小(字节),默认 5MB
- chunkRetry: 分片失败后重试次数,默认 2 次
- chunkRetryDelay: 重试间隔时间(毫秒),默认 1000ms
- threads: 并发上传数,默认 3
分片大小选择
分片大小的选择需要权衡多个因素:
javascript
// 小分片(1-2MB)
// 优点:失败重传代价小,适合不稳定网络
// 缺点:请求次数多,HTTP 开销大
chunkSize: 1 * 1024 * 1024
// 中等分片(5-10MB)- 推荐
// 优点:平衡性能和可靠性
// 缺点:适用于大多数场景
chunkSize: 5 * 1024 * 1024
// 大分片(20MB+)
// 优点:请求次数少,HTTP 开销小
// 缺点:失败重传代价大,需要稳定网络
chunkSize: 20 * 1024 * 1024推荐配置
- 文件 < 10MB: 不使用分片
- 文件 10-100MB: 5MB 分片
- 文件 > 100MB: 10MB 分片
- 网络不稳定: 2-3MB 小分片
服务器端处理
分片请求参数
WebUploader Plus 在上传分片时会附加以下参数:
javascript
{
file: File, // 分片文件
chunks: 10, // 总分片数
chunk: 0, // 当前分片序号(从 0 开始)
id: 'WU_FILE_0', // 文件唯一标识
name: 'test.mp4', // 文件名
type: 'video/mp4', // 文件类型
size: 52428800 // 文件总大小
}PHP 服务器端示例
php
<?php
// upload.php - 分片上传处理
$chunk = isset($_POST['chunk']) ? intval($_POST['chunk']) : 0;
$chunks = isset($_POST['chunks']) ? intval($_POST['chunks']) : 1;
$fileId = $_POST['id'];
$fileName = $_POST['name'];
// 创建临时目录
$tempDir = 'temp/' . $fileId;
if (!is_dir($tempDir)) {
mkdir($tempDir, 0755, true);
}
// 保存当前分片
$chunkFile = $tempDir . '/chunk_' . $chunk;
move_uploaded_file($_FILES['file']['tmp_name'], $chunkFile);
// 检查是否所有分片都已上传
$uploadedChunks = glob($tempDir . '/chunk_*');
if (count($uploadedChunks) == $chunks) {
// 合并分片
$finalFile = 'uploads/' . $fileName;
$fp = fopen($finalFile, 'wb');
for ($i = 0; $i < $chunks; $i++) {
$chunkPath = $tempDir . '/chunk_' . $i;
$chunkContent = file_get_contents($chunkPath);
fwrite($fp, $chunkContent);
unlink($chunkPath); // 删除分片
}
fclose($fp);
rmdir($tempDir); // 删除临时目录
// 返回成功响应
echo json_encode([
'success' => true,
'url' => '/uploads/' . $fileName,
'message' => '上传成功'
]);
} else {
// 分片上传中
echo json_encode([
'success' => true,
'message' => '分片 ' . ($chunk + 1) . '/' . $chunks . ' 上传成功'
]);
}
?>Node.js 服务器端示例
javascript
// 使用 Express 和 Multer
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const app = express();
const upload = multer({ dest: 'temp/' });
app.post('/upload', upload.single('file'), async (req, res) => {
const { chunk, chunks, id, name } = req.body;
const chunkIndex = parseInt(chunk);
const totalChunks = parseInt(chunks);
// 创建临时目录
const tempDir = path.join('temp', id);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// 移动分片到临时目录
const chunkPath = path.join(tempDir, `chunk_${chunkIndex}`);
fs.renameSync(req.file.path, chunkPath);
// 检查是否所有分片都已上传
const uploadedChunks = fs.readdirSync(tempDir);
if (uploadedChunks.length === totalChunks) {
// 合并分片
const finalPath = path.join('uploads', name);
const writeStream = fs.createWriteStream(finalPath);
for (let i = 0; i < totalChunks; i++) {
const chunkFile = path.join(tempDir, `chunk_${i}`);
const data = fs.readFileSync(chunkFile);
writeStream.write(data);
fs.unlinkSync(chunkFile); // 删除分片
}
writeStream.end();
fs.rmdirSync(tempDir); // 删除临时目录
res.json({
success: true,
url: '/uploads/' + name,
message: '上传成功'
});
} else {
res.json({
success: true,
message: `分片 ${chunkIndex + 1}/${totalChunks} 上传成功`
});
}
});
app.listen(3000);断点续传
开启分片上传后,WebUploader Plus 自动支持断点续传。
工作原理
- 上传过程中网络中断
- WebUploader 记录已上传的分片
- 重新连接后,跳过已上传的分片
- 从断点处继续上传
前端实现
javascript
var uploader = WebUploader.create({
pick: '#filePicker',
server: '/upload',
chunked: true,
chunkSize: 5 * 1024 * 1024,
// 开启断点续传
chunkRetry: 3
});
// 监听上传中断
uploader.on('uploadError', function(file, reason) {
console.log('上传中断,可以稍后重试');
});
// 重试按钮
$('#retryBtn').on('click', function() {
// 重试会自动跳过已上传的分片
uploader.retry();
});服务器端支持
服务器端需要支持查询已上传的分片:
php
<?php
// check_chunks.php - 查询已上传的分片
$fileId = $_GET['id'];
$tempDir = 'temp/' . $fileId;
$uploadedChunks = [];
if (is_dir($tempDir)) {
$files = glob($tempDir . '/chunk_*');
foreach ($files as $file) {
preg_match('/chunk_(\d+)/', $file, $matches);
$uploadedChunks[] = intval($matches[1]);
}
}
echo json_encode([
'uploaded' => $uploadedChunks
]);
?>前端查询已上传分片:
javascript
uploader.on('uploadBeforeSend', function(block, data, headers) {
// 查询已上传的分片
$.ajax({
url: '/check_chunks',
data: { id: block.file.id },
async: false,
success: function(response) {
if (response.uploaded.indexOf(data.chunk) >= 0) {
// 跳过已上传的分片
return false;
}
}
});
});分片上传进度
显示分片进度
javascript
uploader.on('uploadProgress', function(file, percentage) {
console.log('总进度:', (percentage * 100).toFixed(2) + '%');
// 计算当前分片
var chunkSize = 5 * 1024 * 1024;
var totalChunks = Math.ceil(file.size / chunkSize);
var currentChunk = Math.floor(percentage * totalChunks);
console.log('当前分片:', currentChunk + '/' + totalChunks);
// 更新UI
$('#progress').text((percentage * 100).toFixed(2) + '%');
$('#chunks').text(currentChunk + '/' + totalChunks);
});实时速度计算
javascript
var lastLoaded = 0;
var lastTime = Date.now();
uploader.on('uploadProgress', function(file, percentage) {
var loaded = file.size * percentage;
var currentTime = Date.now();
// 计算速度(字节/秒)
var speed = (loaded - lastLoaded) / ((currentTime - lastTime) / 1000);
lastLoaded = loaded;
lastTime = currentTime;
// 格式化速度
var speedText = formatSpeed(speed);
$('#speed').text(speedText);
});
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) {
return bytesPerSecond.toFixed(2) + ' B/s';
} else if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(2) + ' KB/s';
} else {
return (bytesPerSecond / 1024 / 1024).toFixed(2) + ' MB/s';
}
}完整示例
html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="webuploader.css">
<style>
.uploader-container {
max-width: 600px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.file-info {
margin: 20px 0;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
.progress-container {
margin: 20px 0;
}
.progress-bar {
height: 20px;
background: #ddd;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #409eff;
width: 0%;
transition: width 0.3s;
}
.stats {
margin-top: 10px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="uploader-container">
<h2>分片上传示例</h2>
<div id="filePicker">选择大文件</div>
<div id="fileInfo" class="file-info" style="display: none;">
<div>文件名: <span id="fileName"></span></div>
<div>文件大小: <span id="fileSize"></span></div>
<div>总分片数: <span id="totalChunks"></span></div>
</div>
<div class="progress-container">
<div class="progress-bar">
<div id="progressFill" class="progress-fill"></div>
</div>
<div class="stats">
<div>进度: <span id="percentage">0%</span></div>
<div>当前分片: <span id="currentChunk">0</span></div>
<div>上传速度: <span id="speed">0 KB/s</span></div>
</div>
</div>
<button id="uploadBtn">开始上传</button>
<button id="pauseBtn">暂停</button>
<button id="retryBtn">重试</button>
</div>
<script src="jquery.min.js"></script>
<script src="webuploader.js"></script>
<script>
var uploader = WebUploader.create({
auto: false,
pick: '#filePicker',
server: '/upload',
chunked: true,
chunkSize: 5 * 1024 * 1024,
chunkRetry: 3,
threads: 3
});
var lastLoaded = 0;
var lastTime = Date.now();
uploader.on('fileQueued', function(file) {
$('#fileInfo').show();
$('#fileName').text(file.name);
$('#fileSize').text(formatSize(file.size));
var totalChunks = Math.ceil(file.size / (5 * 1024 * 1024));
$('#totalChunks').text(totalChunks);
});
uploader.on('uploadProgress', function(file, percentage) {
var loaded = file.size * percentage;
var currentTime = Date.now();
var speed = (loaded - lastLoaded) / ((currentTime - lastTime) / 1000);
lastLoaded = loaded;
lastTime = currentTime;
$('#progressFill').css('width', (percentage * 100) + '%');
$('#percentage').text((percentage * 100).toFixed(2) + '%');
$('#speed').text(formatSpeed(speed));
var chunkSize = 5 * 1024 * 1024;
var currentChunk = Math.floor(percentage * Math.ceil(file.size / chunkSize));
$('#currentChunk').text(currentChunk);
});
uploader.on('uploadSuccess', function(file, response) {
alert('上传成功!');
});
$('#uploadBtn').on('click', function() {
uploader.upload();
});
$('#pauseBtn').on('click', function() {
uploader.stop();
});
$('#retryBtn').on('click', function() {
uploader.retry();
});
function formatSize(size) {
if (size < 1024) return size + ' B';
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
return (size / 1024 / 1024).toFixed(2) + ' MB';
}
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) return bytesPerSecond.toFixed(2) + ' B/s';
if (bytesPerSecond < 1024 * 1024) return (bytesPerSecond / 1024).toFixed(2) + ' KB/s';
return (bytesPerSecond / 1024 / 1024).toFixed(2) + ' MB/s';
}
</script>
</body>
</html>最佳实践
1. 合理设置分片大小
javascript
// 根据文件大小动态调整
function getChunkSize(fileSize) {
if (fileSize < 10 * 1024 * 1024) {
return 0; // 不分片
} else if (fileSize < 100 * 1024 * 1024) {
return 5 * 1024 * 1024; // 5MB
} else {
return 10 * 1024 * 1024; // 10MB
}
}
var file = ...; // 获取文件
var uploader = WebUploader.create({
pick: '#filePicker',
server: '/upload',
chunked: true,
chunkSize: getChunkSize(file.size)
});2. 处理分片失败
javascript
uploader.on('uploadError', function(file, reason) {
console.error('上传失败:', reason);
// 自动重试
setTimeout(function() {
uploader.retry(file);
}, 3000);
});3. 服务器端验证
php
// 服务器端验证分片完整性
$totalSize = 0;
for ($i = 0; $i < $chunks; $i++) {
$chunkFile = $tempDir . '/chunk_' . $i;
if (!file_exists($chunkFile)) {
die(json_encode(['error' => '分片缺失']));
}
$totalSize += filesize($chunkFile);
}
if ($totalSize != $_POST['size']) {
die(json_encode(['error' => '文件大小不匹配']));
}常见问题
为什么要使用分片上传?
- 大文件上传更稳定
- 可以突破服务器限制
- 支持断点续传
- 上传进度更精确
分片大小如何选择?
根据文件大小和网络情况:
- 10MB 以下:不分片
- 10-100MB:5MB 分片
- 100MB 以上:10MB 分片
- 网络不稳定:2-3MB
如何实现秒传?
结合 MD5 计算,参考图片处理中的 MD5 章节。