傳統 <input type="file" multiple> 配合 FormData + AJAX 即可實現現代化多檔上傳。本文提供前後端完整代碼:前端採用 jQuery + 原生 FormData (不需外掛),後端 PHP 處理 $_FILES 陣列、驗證類型大小、回傳 JSON 結果。
<div id="uploader">
<input type="file" id="fileInput" name="myfile[]" multiple accept="image/*,.pdf,.doc,.zip" />
<button id="btnUpload" disabled>開始上傳</button>
<div id="fileList"></div>
<div id="progressBar" style="display:none;">
<div id="progressFill" style="width:0%; height:20px; background:#4CAF50;"></div>
</div>
<div id="status"></div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function() {
let files = [];
// 選檔事件
$('#fileInput').on('change', function(e) {
files = Array.from(e.target.files);
renderFileList();
$('#btnUpload').prop('disabled', files.length === 0);
});
// 渲染待上傳清單
function renderFileList() {
let html = '<ul>';
files.forEach((f, i) => {
html += `<li data-index="${i}">${f.name} (${(f.size/1024).toFixed(1)} KB) <span class="state">待上傳</span></li>`;
});
html += '</ul>';
$('#fileList').html(html);
}
// 上傳按鈕
$('#btnUpload').on('click', async function() {
if (files.length === 0) return;
$(this).prop('disabled', true);
$('#progressBar').show();
$('#status').html('');
const formData = new FormData();
files.forEach(f => formData.append('myfile[]', f));
try {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload.php', true);
// 進度條
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
$('#progressFill').css('width', pct + '%');
}
};
xhr.onload = function() {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
handleResult(res);
} else {
$('#status').html('<span style="color:red">伺服器錯誤: ' + xhr.status + '</span>');
}
};
xhr.onerror = () => $('#status').html('<span style="color:red">網路錯誤</span>');
xhr.send(formData);
} catch (err) {
$('#status').html('<span style="color:red">' + err.message + '</span>');
}
});
function handleResult(res) {
if (res.success) {
$('#status').html('<span style="color:green">全部上傳成功</span>');
$('#fileList li .state').text('成功').css('color', 'green');
$('#fileInput').val(''); files = []; $('#btnUpload').prop('disabled', true);
} else {
$('#status').html('<span style="color:red">' + (res.message || '上傳失敗') + '</span>');
if (res.errors) {
res.errors.forEach(err => {
$(`#fileList li[data-index="${err.index}"] .state`).text('失敗: ' + err.msg).css('color', 'red');
});
}
}
}
});
</script>
<?php
header('Content-Type: application/json; charset=utf-8');
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$allowedTypes = ['image/jpeg','image/png','image/gif','application/pdf','application/zip','application/msword'];
$maxSize = 5 * 1024 * 1024; // 5MB
$maxFiles = 20;
$response = ['success' => false, 'message' => '', 'errors' => [], 'files' => []];
// 檢查是否有檔案
if (empty($_FILES['myfile']['name'][0])) {
$response['message'] = '未收到檔案';
echo json_encode($response);
exit;
}
$fileCount = count($_FILES['myfile']['name']);
if ($fileCount > $maxFiles) {
$response['message'] = "超過最大檔案數限制 ($maxFiles)";
echo json_encode($response);
exit;
}
$allOk = true;
for ($i = 0; $i < $fileCount; $i++) {
$err = null;
$name = $_FILES['myfile']['name'][$i];
$tmp = $_FILES['myfile']['tmp_name'][$i];
$size = $_FILES['myfile']['size'][$i];
$type = $_FILES['myfile']['type'][$i];
$error = $_FILES['myfile']['error'][$i];
// 驗證
if ($error !== UPLOAD_ERR_OK) {
$err = '上傳錯誤代碼: ' . $error;
} elseif ($size > $maxSize) {
$err = '檔案過大 (限制 ' . ($maxSize/1024/1024) . 'MB)';
} elseif (!in_array($type, $allowedTypes)) {
$err = '檔案類型不允許: ' . $type;
} else {
// 檔名安全化
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$safeName = bin2hex(random_bytes(8)) . '.' . $ext;
$dest = $uploadDir . $safeName;
if (move_uploaded_file($tmp, $dest)) {
$response['files'][] = ['original' => $name, 'saved' => $safeName, 'size' => $size, 'type' => $type];
} else {
$err = '伺服器儲存失敗';
}
}
if ($err) {
$allOk = false;
$response['errors'][] = ['index' => $i, 'name' => $name, 'msg' => $err];
}
}
$response['success'] = $allOk;
$response['message'] = $allOk ? '所有檔案上傳成功' : '部分檔案上傳失敗';
echo json_encode($response);
bin2hex(random_bytes(8)) 產生隨機檔名,防止路徑遍歷、覆蓋、特殊字元攻擊。$_FILES['type'] 不夠,建議搭配 finfo_file() 偵測真實 MIME。php_flag engine off (Apache) 或 location ~ \.php$ { deny all; } (Nginx) 禁止腳本執行。upload_max_filesize / post_max_size + 程式碼邏輯 $maxSize。單檔 > 100MB 建議採用 Plupload、Uppy 或 WebUploader 等前端庫,配合後端分塊合併邏輯:
chunk_index, total_chunks, file_hash。cat chunk_* > final_file 並驗證雜湊。