在处理大文件上传时,传统的表单上传方式会遇到各种限制和问题。WebUploader结合PHP的分片上传方案能够有效解决这些痛点,其核心原理可以概括为以下几个关键点:
分片上传的核心思想是将大文件切割成多个小块(chunk),然后逐个上传这些小块,最后在服务器端将这些小块重新组合成完整的文件。具体流程如下:
前端分片处理:
分片上传过程:
服务器端处理:
分片合并:
分片上传相比传统上传方式具有以下显著优势:
WebUploader作为专业的上传组件,在分片上传方面提供了完善的支持:
首先需要在项目中引入WebUploader:
html复制<!-- 引入WebUploader CSS -->
<link rel="stylesheet" type="text/css" href="webuploader.css">
<!-- 引入WebUploader JS -->
<script src="webuploader.min.js"></script>
<!-- 上传容器 -->
<div id="uploader-container">
<div id="filePicker">选择文件</div>
<div id="fileList"></div>
<button id="uploadBtn">开始上传</button>
</div>
以下是针对视频分片上传的推荐配置:
javascript复制var uploader = WebUploader.create({
// 基本配置
swf: 'Uploader.swf', // Flash文件路径,用于兼容老浏览器
server: 'upload.php', // 服务器端接收URL
pick: '#filePicker', // 选择文件的按钮
fileVal: 'file', // 文件字段名
// 分片配置
chunked: true, // 开启分片上传
chunkSize: 5 * 1024 * 1024, // 分片大小5MB
chunkRetry: 2, // 失败重试次数
threads: 3, // 并发上传线程数
// 文件限制
accept: {
title: '视频文件',
extensions: 'mp4,avi,mov,wmv,flv',
mimeTypes: 'video/*'
},
fileSingleSizeLimit: 2 * 1024 * 1024 * 1024 // 2GB
});
WebUploader提供了丰富的事件用于控制上传流程:
javascript复制// 文件加入队列
uploader.on('fileQueued', function(file) {
// 添加到文件列表显示
$('#fileList').append(
`<div id="${file.id}" class="file-item">
<span>${file.name}</span>
<span class="progress">等待上传</span>
</div>`
);
// 计算文件MD5(用于断点续传)
uploader.md5File(file).then(function(md5) {
file.md5 = md5;
});
});
// 分片上传前
uploader.on('uploadBeforeSend', function(obj, data, headers) {
// 添加分片信息到FormData
data.md5 = obj.file.md5;
data.chunk = obj.chunk;
data.chunks = obj.chunks;
});
// 上传进度
uploader.on('uploadProgress', function(file, percentage) {
var $item = $('#' + file.id);
$item.find('.progress').text('上传中: ' + Math.round(percentage * 100) + '%');
});
// 上传成功
uploader.on('uploadSuccess', function(file) {
$('#' + file.id).find('.progress').text('上传完成');
});
// 上传完成(所有分片上传完毕)
uploader.on('uploadComplete', function(file) {
// 通知服务器合并文件
$.post('merge.php', {
name: file.name,
md5: file.md5,
chunks: file.chunks,
size: file.size
}, function(response) {
if(response.success) {
$('#' + file.id).find('.progress').text('合并完成');
}
});
});
断点续传的关键是记录已上传的分片信息:
javascript复制// 检查已上传分片
function checkChunks(md5, chunks) {
return $.ajax({
url: 'check.php',
type: 'POST',
data: {md5: md5},
dataType: 'json'
}).then(function(res) {
return res.uploaded || [];
});
}
// 在文件加入队列后
uploader.on('fileQueued', function(file) {
// ...之前代码...
// 检查已上传分片
checkChunks(file.md5, file.chunks).then(function(uploaded) {
file.uploaded = uploaded;
// 跳过已上传分片
uploader.skipFile(file, uploaded);
});
});
创建upload.php处理分片上传:
php复制<?php
header('Content-Type: application/json');
// 获取参数
$chunk = isset($_REQUEST['chunk']) ? intval($_REQUEST['chunk']) : 0;
$chunks = isset($_REQUEST['chunks']) ? intval($_REQUEST['chunks']) : 1;
$md5 = isset($_REQUEST['md5']) ? $_REQUEST['md5'] : '';
$fileName = isset($_FILES['file']['name']) ? $_FILES['file']['name'] : '';
// 创建临时目录
$tempDir = 'uploads/temp/' . $md5;
if (!file_exists($tempDir)) {
mkdir($tempDir, 0777, true);
}
// 移动分片文件
$chunkPath = $tempDir . '/' . $chunk;
if (move_uploaded_file($_FILES['file']['tmp_name'], $chunkPath)) {
// 记录已上传分片
file_put_contents($tempDir . '/.progress', $chunk . "\n", FILE_APPEND);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => '移动文件失败']);
}
创建check.php用于检查上传进度:
php复制<?php
header('Content-Type: application/json');
$md5 = isset($_POST['md5']) ? $_POST['md5'] : '';
$tempDir = 'uploads/temp/' . $md5;
$uploaded = [];
if (file_exists($tempDir . '/.progress')) {
$uploaded = file($tempDir . '/.progress', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$uploaded = array_map('intval', $uploaded);
}
echo json_encode(['uploaded' => $uploaded]);
创建merge.php处理分片合并:
php复制<?php
header('Content-Type: application/json');
$fileName = isset($_POST['name']) ? $_POST['name'] : '';
$md5 = isset($_POST['md5']) ? $_POST['md5'] : '';
$chunks = isset($_POST['chunks']) ? intval($_POST['chunks']) : 0;
$fileSize = isset($_POST['size']) ? intval($_POST['size']) : 0;
$tempDir = 'uploads/temp/' . $md5;
$finalPath = 'uploads/' . date('Ymd') . '/' . $fileName;
// 检查是否所有分片都已上传
$uploadedChunks = glob($tempDir . '/*[0-9]');
if (count($uploadedChunks) != $chunks) {
echo json_encode(['success' => false, 'error' => '分片不完整']);
exit;
}
// 创建最终目录
if (!file_exists(dirname($finalPath))) {
mkdir(dirname($finalPath), 0777, true);
}
// 合并文件
$finalFile = fopen($finalPath, 'wb');
for ($i = 0; $i < $chunks; $i++) {
$chunkPath = $tempDir . '/' . $i;
$chunkContent = file_get_contents($chunkPath);
fwrite($finalFile, $chunkContent);
unlink($chunkPath); // 删除分片
}
fclose($finalFile);
// 验证文件大小
if (filesize($finalPath) != $fileSize) {
unlink($finalPath);
echo json_encode(['success' => false, 'error' => '文件大小不匹配']);
exit;
}
// 清理临时文件
array_map('unlink', glob($tempDir . '/.*'));
rmdir($tempDir);
echo json_encode(['success' => true, 'path' => $finalPath]);
在上传视频时,可以提取第一帧作为封面:
javascript复制// 在文件加入队列后生成预览
uploader.on('fileQueued', function(file) {
// ...之前代码...
// 视频预览
if (file.type.indexOf('video') === 0) {
var video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = function() {
window.URL.revokeObjectURL(video.src);
// 设置视频时长
$('#' + file.id).append('<span class="duration">' +
formatDuration(video.duration) + '</span>');
// 生成封面
generateThumbnail(file, video);
};
video.src = URL.createObjectURL(file.getSource());
}
});
function generateThumbnail(file, video) {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = 160;
canvas.height = 90;
// 截取第一帧
video.currentTime = 0;
video.addEventListener('seeked', function() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 显示缩略图
var thumbnail = canvas.toDataURL('image/jpeg');
$('#' + file.id).prepend(
'<img class="thumb" src="' + thumbnail + '">'
);
});
}
WebUploader支持动态调整上传速度:
javascript复制// 限制上传速度为1MB/s
uploader.uploadSpeed = 1024 * 1024;
// 动态调整速度
$('#speedControl').on('change', function() {
uploader.uploadSpeed = $(this).val() * 1024 * 1024;
});
增强分片上传的可靠性:
javascript复制// 分片上传前验证
uploader.on('uploadBeforeSend', function(obj, data, headers) {
// 添加验证头
headers['X-Chunk-MD5'] = calculateChunkMD5(obj.blob);
});
// 计算分片MD5
function calculateChunkMD5(blob) {
return new Promise(function(resolve) {
var reader = new FileReader();
reader.onload = function(e) {
var md5 = SparkMD5.ArrayBuffer.hash(e.target.result);
resolve(md5);
};
reader.readAsArrayBuffer(blob);
});
}
// PHP端验证分片
$chunkMD5 = $_SERVER['HTTP_X_CHUNK_MD5'] ?? '';
$localMD5 = md5_file($_FILES['file']['tmp_name']);
if ($chunkMD5 && $chunkMD5 !== $localMD5) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => '分片校验失败']);
exit;
}
根据网络状况动态调整并发数:
javascript复制// 根据网络延迟动态调整并发数
var lastSpeed = 0;
var adjustInterval = setInterval(function() {
var currentSpeed = uploader.getStats().speed;
if (currentSpeed > lastSpeed * 1.2) {
// 速度提升,尝试增加并发
uploader.options.threads = Math.min(
uploader.options.threads + 1,
5 // 最大并发数
);
} else if (currentSpeed < lastSpeed * 0.8) {
// 速度下降,减少并发
uploader.options.threads = Math.max(
uploader.options.threads - 1,
1 // 最小并发数
);
}
lastSpeed = currentSpeed;
}, 5000); // 每5秒调整一次
// 上传完成后停止调整
uploader.on('uploadFinished', function() {
clearInterval(adjustInterval);
});
php复制// 在upload.php中添加
$allowedTypes = ['video/mp4', 'video/avi', 'video/quicktime', 'video/x-ms-wmv'];
if (!in_array($_FILES['file']['type'], $allowedTypes)) {
echo json_encode(['success' => false, 'error' => '不支持的文件类型']);
exit;
}
php复制// 使用getimagesize验证视频文件
$info = getimagesize($_FILES['file']['tmp_name']);
if ($info === false || strpos($info['mime'], 'video/') !== 0) {
echo json_encode(['success' => false, 'error' => '无效的视频文件']);
exit;
}
php复制// 在merge.php中
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$newName = md5(uniqid() . $fileName) . '.' . $extension;
$finalPath = 'uploads/' . date('Ymd') . '/' . $newName;
php复制// 在merge.php中使用流式合并
$finalFile = fopen($finalPath, 'wb');
for ($i = 0; $i < $chunks; $i++) {
$chunkPath = $tempDir . '/' . $i;
$chunkFile = fopen($chunkPath, 'rb');
while (!feof($chunkFile)) {
fwrite($finalFile, fread($chunkFile, 8192));
}
fclose($chunkFile);
unlink($chunkPath);
}
fclose($finalFile);
javascript复制// 根据文件大小动态调整分片大小
function calculateChunkSize(fileSize) {
if (fileSize > 1024 * 1024 * 1024) { // >1GB
return 10 * 1024 * 1024; // 10MB
} else if (fileSize > 100 * 1024 * 1024) { // >100MB
return 5 * 1024 * 1024; // 5MB
} else {
return 1 * 1024 * 1024; // 1MB
}
}
uploader.on('fileQueued', function(file) {
uploader.options.chunkSize = calculateChunkSize(file.size);
});
php复制// 在upload.php中添加负载检查
$load = sys_getloadavg();
if ($load[0] > 5) { // 1分钟负载超过5
http_response_code(503);
echo json_encode(['success' => false, 'error' => '服务器繁忙,请稍后重试']);
exit;
}
分片顺序错乱:
分片丢失:
网络中断:
上传大小限制:
code复制upload_max_filesize = 1024M
post_max_size = 1024M
max_execution_time = 300
临时目录权限:
bash复制chmod -R 777 uploads/
内存不足:
code复制memory_limit = 256M
IE浏览器支持:
javascript复制if (!WebUploader.Uploader.support()) {
alert('您的浏览器不支持分片上传,请使用现代浏览器');
}
Safari浏览器问题:
移动端适配:
javascript复制uploader.on('touchstart', function() {
// 处理触摸事件
});
在实际项目中实现视频分片上传时,我总结了以下几点经验:
分片大小选择:
并发控制策略:
断点续传实现:
上传进度显示:
错误处理与重试:
服务器端优化:
安全防护:
日志记录:
在实现过程中,最大的挑战是处理各种边界情况和异常场景。例如网络中断后恢复上传、服务器重启后继续未完成的上传、不同浏览器的兼容性问题等。通过多次迭代和测试,最终形成了一个稳定可靠的分片上传方案。