無刷新上傳是提升用戶體驗的關鍵交互。早期依賴 iframe 或 FormData + XMLHttpRequest,現代瀏覽器原生支援 fetch + FormData,代碼更簡潔、錯誤處理更優雅。本文提供兩種實現方案,並涵蓋進度顯示、錯誤處理、安全驗證、多檔案上傳等生產級細節。
<!DOCTYPE html>
<html>
<head>
AJAX 圖片上傳演示
</head>
<body>
圖片上傳演示
0%
</body>
</html>
upload.php<?php
header('Content-Type: application/json; charset=utf-8');
// 配置
$uploadDir = __DIR__ . '/uploads/';
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
$maxFiles = 10;
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$errors = [];
$uploaded = [];
foreach ($_FILES['images']['tmp_name'] as $index => $tmpName) {
$error = $_FILES['images']['error'][$index];
if ($error !== UPLOAD_ERR_OK) {
$errors[] = "檔案 {$index}: 上傳錯誤代碼 {$error}";
continue;
}
$name = $_FILES['images']['name'][$index];
$size = $_FILES['images']['size'][$index];
$mime = mime_content_type($tmpName); // 比 $_FILES['type'] 可靠
// 伺服器端二次驗證
if (!in_array($mime, $allowedMimes, true)) {
$errors[] = "{$name}: 不支援的檔案類型 ({$mime})";
continue;
}
if ($size > $maxSize) {
$errors[] = "{$name}: 檔案過大 ({$size} bytes)";
continue;
}
// 生成安全檔名:時間戳 + 隨機字串 + 原副檔名
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$safeName = date('YmdHis') . '_' . bin2hex(random_bytes(8)) . ".{$ext}";
$destPath = $uploadDir . $safeName;
if (!move_uploaded_file($tmpName, $destPath)) {
$errors[] = "{$name}: 移動檔案失敗";
continue;
}
$uploaded[] = [
'original_name' => $name,
'saved_name' => $safeName,
'url' => '/uploads/' . $safeName,
'size' => $size,
'mime' => $mime,
];
}
echo json_encode([
'success' => empty($errors),
'message' => empty($errors) ? '上傳成功' : '部分檔案上傳失敗',
'data' => $uploaded,
'errors' => $errors,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
核心邏輯與方案一的 uploadWithProgress 函數相同,僅差異在 FormData 建構與送出方式。現代專案建議直接使用方案一,僅在需支援 IE10 以下時考慮方案二。
| 維度 | 關鍵措施 |
|---|---|
| 安全驗證 | 客戶端攔截 + 伺服器端 mime_content_type() 二次驗證 + 副檔名白名單 + 檔案大小限制 |
| 檔名安全 | 絕不使用用戶原始檔名,採用 時間戳+隨機字串+副檔名 或 UUID |
| 儲存隔離 | 上傳目錄設定 php_flag engine off / Nginx location ~ \.php$ { deny all; } 禁止腳本執行 |
| 進度體驗 | XMLHttpRequest.upload.onprogress 實時更新進度條 |
| 錯誤處理 | 網路錯誤、HTTP 非 2xx、業務失敗、格式錯誤分層處理,用戶可見錯誤訊息 |
| 多檔案 | multiple 屬性 + FormData.append('images[]', file) 陣列式上傳 |
| 大檔案 | 分片上傳(WebUploader / Uppy / 自製 blob.slice + 合併 API) |
| CDN 直傳 | 簽名直傳 OSS/S3,減輕應用伺服器頻寬壓力 |
$_FILES['images']['type'] 來自瀏覽器,不可信,必須用 mime_content_type() 或 finfo_file() 驗證真實 MIMEmove_uploaded_file() 目標目錄需 PHP 進程寫入權限,且不可在 Web 可執行目錄.php.jpg 繞過檢查被執行max_file_uploads (php.ini, 預設 20) 限制單次上傳檔案數post_max_size 必須大於 upload_max_filesize掌握細節,才能寫出既好用又安全的上傳功能。