在 HTML5 <input type="file"> 原生支持 progress 事件之前,PHP 5.2+ 引入的 APC 擴展提供了 APC_UPLOAD_PROGRESS 機制,成為當時實現上傳進度條的主流方案。雖然現代項目多改用前端 XMLHttpRequest.upload.onprogress 或 fetch 配合後端流式處理,但理解 APC 方案有助於掌握 PHP 會話級緩存與文件上傳底層機制。本文系統梳理原理、配置、前後端完整代碼及避坑指南。
APC(Alternative PHP Cache)是 PHP 的 opcode 緩存擴展。自 PHP 5.2 起,APC 新增 APC_UPLOAD_PROGRESS 功能:
<input type="hidden" name="APC_UPLOAD_PROGRESS" value="唯一ID">$_POST['APC_UPLOAD_PROGRESS'],以 upload_唯一ID 為鍵在 APC 共享內存中寫入進度數據current(已上傳字節)、total(總字節)、rate(速度)等字段關鍵優勢:臨時文件達閾值自動落盤,避免大文件佔滿內存;無需修改 PHP 核心,僅需擴展配置。
在 php.ini 中配置 [apc] 區段,核心參數如下:
| 參數 | 默認值 | 說明 | 建議生產值 |
|---|---|---|---|
| apc.enabled | 1 | 啟用/禁用 APC | 1 |
| apc.shm_size | 32M | 共享內存大小,單位 MB | 128M~256M(視上傳並發) |
| apc.upload_progress.enabled | 1 | 啟用上傳進度追蹤 | 1 |
| apc.upload_progress.prefix | upload_ | 緩存鍵前綴 | upload_ |
| apc.upload_progress.freq | 0 | 更新頻率(0=盡可能頻繁) | 0 或 1024 |
| apc.upload_progress.min_size | 1M | 啟用進度追蹤的最小文件 | 1M |
| apc.ttl | 0 | 緩存條目生存秒數,0=永不失效(風險) | 3600~7200 |
| apc.gc_ttl | 3600 | 垃圾回收生存時間 | 3600 |
| apc.max_file_size | 1M | 單文件緩存上限 | 按需調大 |
注意:APC 在 PHP 5.5+ 被 Zend OpCache 取代,上傳進度功能已移至 PHP 核心 Session 上傳進度(session.upload_progress.enabled),新項目請優先使用 Session 方案或前端原生進度事件。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>文件上傳進度演示</title>
<style>
#progressouter { display:none; width:400px; height:20px; border:1px solid #ccc; border-radius:4px; overflow:hidden; }
#progressinner { width:0%; height:100%; background:#4CAF50; transition:width 0.3s; }
#upstatus { margin-top:8px; font-size:14px; color:#333; }
</style>
</head>
<body>
<form id="upload_form" action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="upid_<?php echo uniqid(); ?>">
視頻標題:<input type="text" name="subject" required><br>
視頻說明:<input type="text" name="content"><br>
視頻文件:<input type="file" name="upfile" id="upfile" required><br>
<input type="submit" value="開始上傳" onclick="startProgress('upid_<?php echo uniqid(); ?>'); return true;">
</form>
<div id="upstatus"></div>
<div id="progressouter"><div id="progressinner"></div></div>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script>
function startProgress(upid) {
$('#progressouter').show();
$('#upstatus').html('準備上傳...');
setTimeout(function(){ getProgress(upid); }, 100);
}
function getProgress(upid) {
$.getJSON('progress.php', { progress_key: upid }, function(json) {
if (!json || json.total === 0) {
$('#upstatus').html('等待上傳開始...');
setTimeout(function(){ getProgress(upid); }, 500);
return;
}
var percent = Math.min(100, Math.round(json.current / json.total * 100));
$('#progressinner').css('width', percent + '%');
$('#upstatus').html('文件大小: ' + (json.total/1024/1024).toFixed(2) + ' MB | 已上傳: ' + (json.current/1024/1024).toFixed(2) + ' MB (' + percent + '%)');
if (percent < 100) {
setTimeout(function(){ getProgress(upid); }, 200);
} else {
$('#upstatus').html('上傳完成,服務器處理中...');
}
}).fail(function() {
$('#upstatus').html('進度查詢失敗,重試中...');
setTimeout(function(){ getProgress(upid); }, 1000);
});
}
</script>
</body>
</html>
<?php
// progress.php
header('Content-Type: application/json; charset=utf-8');
if (empty($_GET['progress_key'])) {
echo json_encode(['error' => 'Missing progress_key']);
exit;
}
$key = 'upload_' . $_GET['progress_key'];
$status = apc_fetch($key);
if ($status === false || !isset($status['total'])) {
// 上傳未開始或已結束
echo json_encode(['total' => 0, 'current' => 0, 'rate' => 0, 'done' => false]);
exit;
}
echo json_encode([
'total' => (int)$status['total'],
'current' => (int)$status['current'],
'rate' => (int)($status['rate'] ?? 0),
'done' => $status['done'] ?? false
]);
?>
<?php
// upload.php
// 實際業務邏輯:驗證、移動、入庫、生成縮略圖等
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upfile'])) {
$file = $_FILES['upfile'];
if ($file['error'] !== UPLOAD_ERR_OK) {
die('上傳錯誤代碼: ' . $file['error']);
}
// 簡單驗證:類型、大小
$allowed = ['video/mp4', 'video/x-msvideo', 'video/quicktime'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
if (!in_array($mime, $allowed)) {
die('不支持的文件類型');
}
if ($file['size'] > 500 * 1024 * 1024) { // 500MB
die('文件過大');
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$dest = $uploadDir . uniqid('vid_') . '.' . $ext;
if (move_uploaded_file($file['tmp_name'], $dest)) {
echo "上傳成功:$dest";
// 這裡可入庫、發送隊列任務轉碼等
} else {
echo "移動文件失敗";
}
}
?>
apc.upload_progress.enabled=1、表單 enctype="multipart/form-data"、隱藏域 name="APC_UPLOAD_PROGRESS" 在 <input type="file"> 之前。apc.rfc1867 未啟用(舊版參數)、文件過小未達 min_size 閾值。upload_max_filesize、post_max_size、max_execution_time、max_input_time、Nginx client_max_body_size。progress_key(用 uniqid() 或 UUID),前端輪詢攜帶正確 key。pecl install apcu(僅用戶緩存)+ 核心 session.upload_progress,或直接用前端 xhr.upload.onprogress 方案。| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| APC_UPLOAD_PROGRESS | PHP 擴展共享內存 | 無需前端特殊支持、PHP 5.2+ 兼容 | 依賴擴展、PHP 7+ 棄用、調試困難 | 老項目維護、無法升級 PHP 版本 |
| Session Upload Progress | PHP 核心 Session 存儲 | 無額外擴展、配置簡單、PHP 5.4+ 內置 | 需啟用 Session、文件鎖可能阻塞 | 中小型項目、傳統表單上傳 |
| XHR/Fetch 原生進度 | 瀏覽器原生事件 | 無後端依賴、實時性最好、支持暫停續傳 | 需前端 JS 實現、不支持傳統 form 提交 | 現代 SPA、大文件分片上傳、斷點續傳 |
| WebUploader / Uppy / FineUploader | 成熟前端庫封裝 | 分片、並發、斷點、預覽、國際化開箱即用 | 引入體積、學習成本 | 生產級文件上傳系統 |
APC 上傳進度方案是 PHP 早期解決大文件上傳用戶體驗的經典實踐,展示了「共享內存 + 輪詢」的架構思想。現代項目建議採用 前端原生進度事件 + 後端分片/流式處理 架構,結合 Uppy/WebUploader 等成熟庫,實現斷點續傳、並發分片、秒傳等企業級功能。理解歷史方案有助於在遷移重構時平滑過渡,也能在面試中展現技術演進視野。