PHP格式化文件上传进度的显示技巧
在Web开发中,用户上传大文件时如果没有进度反馈,很容易产生等待焦虑,甚至误以为上传失败。PHP本身提供了多种方式获取文件上传进度,我们可以将其做格式化处理,以更直观的方式展示给用户。本文将介绍两种常用的实现思路,附带完整代码示例。
一、使用Session存储上传进度(PHP 5.4+原生支持)
PHP 5.4及以上版本内置了upload progress功能,只需要在php.ini中开启相关配置,就可以通过Session获取上传进度信息。首先我们需要确认php.ini中的配置是否正确:
- upload_progress.enabled = On
- upload_progress.prefix = "upload_progress_"
- upload_progress.name = "UPLOAD_PROGRESS"
- session.upload_progress.enabled = On
- session.upload_progress.cleanup = On
前端上传表单需要添加一个隐藏字段,字段名对应配置的upload_progress.name,值可以自定义为本次上传的唯一标识:
<!-- 前端上传表单示例 -->
<form action="upload.php" method="post" enctype="multipart/form-data" id="uploadForm">
<!-- 必须添加的上传进度标识字段 -->
<input type="hidden" name="UPLOAD_PROGRESS" value="upload_123456"/>
<input type="file" name="upload_file" id="upload_file"/>
<button type="submit">开始上传</button>
</form>
<div id="progressBox" style="width: 300px; height: 20px; border: 1px solid #ccc; margin-top: 10px;">
<div id="progressBar" style="height: 100%; width: 0%; background-color: #4CAF50;"></div>
</div>
<p id="progressText">等待上传开始...</p>
<script>
// 监听表单提交,轮询获取上传进度
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const xhr = new XMLHttpRequest();
// 监听上传进度事件
xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
updateProgress(percent, event.loaded, event.total);
}
});
// 上传完成处理
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
updateProgress(100, 0, 0);
document.getElementById('progressText').innerText = '上传完成';
}
});
xhr.open('POST', 'upload.php');
xhr.send(formData);
// 同时轮询Session中的进度信息(针对部分浏览器不支持upload事件的场景)
const progressInterval = setInterval(function() {
fetch('get_progress.php?progress_key=upload_123456')
.then(res => res.json())
.then(data => {
if (data && data.content_length) {
const percent = Math.round((data.bytes_processed / data.content_length) * 100);
updateProgress(percent, data.bytes_processed, data.content_length);
}
if (data && data.done) {
clearInterval(progressInterval);
}
});
}, 500);
});
// 更新进度显示的函数
function updateProgress(percent, loaded, total) {
document.getElementById('progressBar').style.width = percent + '%';
if (total > 0) {
const loadedMB = (loaded / 1024 / 1024).toFixed(2);
const totalMB = (total / 1024 / 1024).toFixed(2);
document.getElementById('progressText').innerText = `已上传 ${percent}%(${loadedMB}MB / ${totalMB}MB)`;
} else {
document.getElementById('progressText').innerText = `已上传 ${percent}%`;
}
}
</script>后端获取进度的接口get_progress.php代码如下,这里会对进度信息做格式化处理,返回前端需要的结构化数据:
<?php
// get_progress.php 获取上传进度
session_start();
$progressKey = $_GET['progress_key'] ?? '';
$progressPrefix = 'upload_progress_'; // 对应php.ini中的upload_progress.prefix
$fullKey = $progressPrefix . $progressKey;
$progressData = [];
if (isset($_SESSION[$fullKey])) {
$sessionProgress = $_SESSION[$fullKey];
// 格式化进度数据,避免返回多余信息
$progressData = [
'bytes_processed' => $sessionProgress['bytes_processed'] ?? 0,
'content_length' => $sessionProgress['content_length'] ?? 0,
'done' => $sessionProgress['done'] ?? false,
// 格式化后的上传速度(字节/秒)
'speed' => $sessionProgress['bytes_processed'] > 0 && isset($sessionProgress['start_time'])
? $sessionProgress['bytes_processed'] / (time() - $sessionProgress['start_time'])
: 0
];
}
// 清理已完成的进度Session,避免占用空间
if (isset($progressData['done']) && $progressData['done']) {
unset($_SESSION[$fullKey]);
}
header('Content-Type: application/json');
echo json_encode($progressData);处理上传的upload.php代码如下,负责接收文件并做基础校验:
<?php
// upload.php 处理文件上传
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upload_file'])) {
$file = $_FILES['upload_file'];
// 基础校验
if ($file['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['code' => 500, 'msg' => '上传失败,错误码:' . $file['error']]);
exit;
}
// 自定义上传目录,确保目录存在
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$savePath = $uploadDir . basename($file['name']);
// 移动临时文件到目标目录
if (move_uploaded_file($file['tmp_name'], $savePath)) {
echo json_encode(['code' => 200, 'msg' => '上传成功', 'path' => $savePath]);
} else {
echo json_encode(['code' => 500, 'msg' => '文件保存失败']);
}
exit;
}
echo json_encode(['code' => 400, 'msg' => '无效请求']);二、自定义进度存储(兼容低版本PHP)
如果使用的PHP版本低于5.4,或者需要更灵活的进度控制,我们可以通过自定义临时文件存储上传进度。这种方式需要前端配合分块上传,或者监听上传流的进度,后端每收到一部分数据就更新进度文件。
前端分块上传的示例代码如下,我们将大文件分成多个2MB的块依次上传,每次上传后更新进度:
<!-- 分块上传表单 -->
<input type="file" id="chunkFile" />
<button onclick="startChunkUpload()">开始分块上传</button>
<div id="chunkProgressBox" style="width: 300px; height: 20px; border: 1px solid #ccc; margin-top: 10px;">
<div id="chunkProgressBar" style="height: 100%; width: 0%; background-color: #2196F3;"></div>
</div>
<p id="chunkProgressText">等待上传开始...</p>
<script>
let chunkSize = 2 * 1024 * 1024; // 每块2MB
let currentChunk = 0;
let file = null;
let uploadId = 'chunk_' + Date.now(); // 本次上传唯一标识
function startChunkUpload() {
file = document.getElementById('chunkFile').files[0];
if (!file) {
alert('请选择文件');
return;
}
currentChunk = 0;
uploadNextChunk();
}
function uploadNextChunk() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('upload_chunk', chunk);
formData.append('upload_id', uploadId);
formData.append('chunk_index', currentChunk);
formData.append('total_chunks', Math.ceil(file.size / chunkSize));
formData.append('file_name', file.name);
formData.append('file_size', file.size);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'chunk_upload.php');
xhr.send(formData);
xhr.addEventListener('load', function() {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
if (res.code === 200) {
// 计算当前总进度
const totalLoaded = (currentChunk + 1) * chunkSize > file.size ? file.size : (currentChunk + 1) * chunkSize;
const percent = Math.round((totalLoaded / file.size) * 100);
const loadedMB = (totalLoaded / 1024 / 1024).toFixed(2);
const totalMB = (file.size / 1024 / 1024).toFixed(2);
document.getElementById('chunkProgressBar').style.width = percent + '%';
document.getElementById('chunkProgressText').innerText = `已上传 ${percent}%(${loadedMB}MB / ${totalMB}MB)`;
currentChunk++;
if (currentChunk < Math.ceil(file.size / chunkSize)) {
uploadNextChunk(); // 继续上传下一块
} else {
document.getElementById('chunkProgressText').innerText = '所有分块上传完成,正在合并文件...';
mergeChunks();
}
}
}
});
}
// 通知后端合并所有分块
function mergeChunks() {
const formData = new FormData();
formData.append('action', 'merge');
formData.append('upload_id', uploadId);
formData.append('file_name', file.name);
formData.append('total_chunks', Math.ceil(file.size / chunkSize));
fetch('chunk_upload.php', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
document.getElementById('chunkProgressText').innerText = '文件上传完成,路径:' + data.path;
} else {
document.getElementById('chunkProgressText').innerText = '合并失败:' + data.msg;
}
});
}
</script>后端处理分块上传和合并的chunk_upload.php代码如下,进度信息存储在临时文件中,方便随时读取:
<?php
// chunk_upload.php 处理分块上传和合并
$uploadDir = __DIR__ . '/uploads/';
$progressDir = __DIR__ . '/progress/';
// 确保目录存在
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if (!is_dir($progressDir)) mkdir($progressDir, 0755, true);
$action = $_POST['action'] ?? '';
if ($action === 'merge') {
// 合并分块文件
$uploadId = $_POST['upload_id'] ?? '';
$fileName = $_POST['file_name'] ?? '';
$totalChunks = intval($_POST['total_chunks'] ?? 0);
$chunkDir = $uploadDir . $uploadId . '/';
$targetPath = $uploadDir . $fileName;
// 打开目标文件准备写入
$fp = fopen($targetPath, 'wb');
for ($i = 0; $i < $totalChunks; $i++) {
$chunkPath = $chunkDir . $i . '.chunk';
if (file_exists($chunkPath)) {
$chunkContent = file_get_contents($chunkPath);
fwrite($fp, $chunkContent);
unlink($chunkPath); // 删除已合并的分块
}
}
fclose($fp);
// 删除分块目录和进度文件
rmdir($chunkDir);
$progressFile = $progressDir . $uploadId . '.json';
if (file_exists($progressFile)) unlink($progressFile);
echo json_encode(['code' => 200, 'msg' => '合并完成', 'path' => $targetPath]);
exit;
}
// 处理分块上传
$uploadId = $_POST['upload_id'] ?? '';
$chunkIndex = intval($_POST['chunk_index'] ?? 0);
$totalChunks = intval($_POST['total_chunks'] ?? 0);
$fileSize = intval($_POST['file_size'] ?? 0);
$chunkDir = $uploadDir . $uploadId . '/';
if (!is_dir($chunkDir)) mkdir($chunkDir, 0755, true);
$chunkPath = $chunkDir . $chunkIndex . '.chunk';
// 保存当前分块
if (isset($_FILES['upload_chunk']) && $_FILES['upload_chunk']['error'] === UPLOAD_ERR_OK) {
move_uploaded_file($_FILES['upload_chunk']['tmp_name'], $chunkPath);
// 更新进度文件
$currentLoaded = ($chunkIndex + 1) * min(2 * 1024 * 1024, $fileSize - $chunkIndex * 2 * 1024 * 1024);
$progressData = [
'upload_id' => $uploadId,
'file_size' => $fileSize,
'bytes_processed' => $currentLoaded,
'percent' => round(($currentLoaded / $fileSize) * 100, 2),
'last_update' => time()
];
file_put_contents($progressDir . $uploadId . '.json', json_encode($progressData));
echo json_encode(['code' => 200, 'msg' => '分块上传成功', 'chunk_index' => $chunkIndex]);
} else {
echo json_encode(['code' => 500, 'msg' => '分块上传失败']);
}如果需要单独获取分块上传的进度,可以新增一个进度查询接口:
<?php
// get_chunk_progress.php 查询分块上传进度
$progressDir = __DIR__ . '/progress/';
$uploadId = $_GET['upload_id'] ?? '';
$progressFile = $progressDir . $uploadId . '.json';
if (file_exists($progressFile)) {
$progressData = json_decode(file_get_contents($progressFile), true);
// 格式化速度信息
if (isset($progressData['last_update']) && time() - $progressData['last_update'] < 10) {
$progressData['speed'] = $progressData['bytes_processed'] / (time() - $progressData['last_update']);
} else {
$progressData['speed'] = 0;
}
header('Content-Type: application/json');
echo json_encode($progressData);
} else {
echo json_encode(['code' => 404, 'msg' => '未找到上传进度']);
}三、进度格式化的常见优化点
实际使用中,我们可以对进度信息做更多人性化格式化:
- 文件大小换算:将字节数转换为合适的单位,比如小于1MB用KB,小于1GB用MB,避免显示过长的数字
- 上传速度显示:每秒上传的字节数转换为KB/s、MB/s,方便用户预估剩余时间
- 剩余时间计算:根据当前速度和剩余字节数,估算还需要多久上传完成
- 异常进度处理:如果检测到进度长时间不更新,提示用户检查网络或重新上传
以下是一个文件大小格式化的通用PHP函数示例,可以直接在进度处理中使用:
<?php
/**
* 格式化字节数为可读的文件大小
* @param int $bytes 字节数
* @param int $precision 保留小数位数
* @return string 格式化后的字符串
*/
function formatFileSize($bytes, $precision = 2) {
if ($bytes <= 0) return '0B';
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$pow = floor(log($bytes, 1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . $units[$pow];
}
// 使用示例
$size = 12345678; // 约11.77MB
echo formatFileSize($size); // 输出 11.77MB通过上述方法,我们可以实现从基础到自定义的PHP文件上传进度格式化显示,适配不同的项目需求。开发者可以根据实际的PHP版本和业务场景,选择合适的方式实现进度反馈功能。