PHP文件上传进度格式化技巧及完整实现方案

来源:IPIPP.com作者:陈平安
导读:本期聚焦于小伙伴创作的《PHP文件上传进度格式化技巧及完整实现方案》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《PHP文件上传进度格式化技巧及完整实现方案》有用,将其分享出去将是对创作者最好的鼓励。

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版本和业务场景,选择合适的方式实现进度反馈功能。

PHP文件上传进度进度条格式化分块上传Session上传进度upload_progress

免责声明:已尽一切努力确保本网站所含信息的准确性。网站部分内容来源于网络或由用户自行发表,内容观点不代表本站立场。本站是个人网站免费分享,内容仅供个人学习、研究或参考使用,如内容中引用了第三方作品,其版权归原作者所有。若内容触犯了您的权益,请联系我们进行处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。前端、网络、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握网站开发与运维所需的核心技术栈。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端逻辑,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。