首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP + Ajax 檔案上傳完整實作:jquery.ajaxFileUpload、進度條、多檔、安全驗證

     編輯:PHP 相關知識

前言

傳統 `

` 提交會導致頁面跳轉,現代 Web 應用普遍採用 Ajax 非同步上傳,配合進度條、拖曳上傳、多檔佇列、即時預覽。本文提供兩種方案:傳統 iframe 模擬 Ajax(相容舊瀏覽器)與現代 Fetch/XHR + FormData(標準、支援進度事件),並涵蓋後端 PHP 驗證、儲存、錯誤處理、安全防護完整流程。

方案一:現代標準 — Fetch + FormData(推薦)

前端:上傳元件(支援多檔、進度、預覽、拖曳)

<!-- upload.html -->
<div id="dropzone" class="dropzone">
  拖曳檔案到此,或 <input type="file" id="fileInput" multiple hidden><label for="fileInput">點選檔案</label>
</div>
<div id="fileList"></div>
<button id="btnUpload" disabled>開始上傳</button>
<div id="progressWrap" style="display:none">
  <progress id="overallProgress" max="100" value="0"></progress>
  <span id="progressText">0%</span>
</div>

<style>
.dropzone { border: 2px dashed #ccc; padding: 40px; text-align: center; cursor: pointer; }
.dropzone.dragover { border-color: #007bff; background: #f0f8ff; }
#fileList .file-item { display: flex; align-items: center; gap: 10px; padding: 8px; border-bottom: 1px solid #eee; }
#fileList .file-item .preview { width: 40px; height: 40px; object-fit: cover; }
#fileList .file-item .bar { flex: 1; height: 4px; background: #eee; border-radius: 2px; overflow: hidden; }
#fileList .file-item .bar > div { height: 100%; background: #007bff; width: 0%; transition: width 0.3s; }
</style>

<script>
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const btnUpload = document.getElementById('btnUpload');
const overallProgress = document.getElementById('overallProgress');
const progressText = document.getElementById('progressText');
const progressWrap = document.getElementById('progressWrap');

let filesQueue = []; // {file, xhr, abortController, status: 'pending'|'uploading'|'done'|'error'}

['dragenter', 'dragover'].forEach(e => dropzone.addEventListener(e, ev => {
    ev.preventDefault(); dropzone.classList.add('dragover');
}));
['dragleave', 'drop'].forEach(e => dropzone.addEventListener(e, ev => {
    ev.preventDefault(); dropzone.classList.remove('dragover');
}));
dropzone.addEventListener('drop', ev => {
    addFiles(ev.dataTransfer.files);
});
fileInput.addEventListener('change', ev => {
    addFiles(ev.target.files);
    ev.target.value = ''; // 允許重複選同檔
});

function addFiles(fileListObj) {
    [...fileListObj].forEach(file => {
        if (filesQueue.some(f => f.file.name === file.name && f.file.size === file.size)) return;
        filesQueue.push({file, status: 'pending'});
        renderFileItem(file, filesQueue.length - 1);
    });
    btnUpload.disabled = filesQueue.length === 0;
}

function renderFileItem(file, index) {
    const div = document.createElement('div');
    div.className = 'file-item';
    div.dataset.index = index;
    const isImg = file.type.startsWith('image/');
    div.innerHTML = `
        ${isImg ? `` : '📄'}
        
${file.name}
${(file.size/1024).toFixed(1)} KB
待上傳 `; fileList.appendChild(div); } fileList.addEventListener('click', ev => { if (ev.target.matches('.remove')) { const i = +ev.target.dataset.index; filesQueue.splice(i, 1); fileList.innerHTML = ''; filesQueue.forEach((f, idx) => renderFileItem(f.file, idx)); btnUpload.disabled = filesQueue.length === 0; } }); btnUpload.addEventListener('click', async () => { progressWrap.style.display = 'block'; btnUpload.disabled = true; let done = 0; for (let i = 0; i < filesQueue.length; i++) { const item = filesQueue[i]; if (item.status !== 'pending') continue; item.status = 'uploading'; updateStatus(i, '上傳中...'); try { await uploadFile(item.file, (pct) => { updateProgress(i, pct); }); item.status = 'done'; updateStatus(i, '完成 ✓'); } catch (e) { item.status = 'error'; updateStatus(i, '失敗:' + e.message); } done++; overallProgress.value = Math.round(done / filesQueue.length * 100); progressText.textContent = overallProgress.value + '%'; } btnUpload.disabled = false; alert('全部完成'); }); function uploadFile(file, onProgress) { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', file); // 可加其他欄位 // formData.append('folder', 'uploads'); const xhr = new XMLHttpRequest(); xhr.open('POST', 'upload.php', true); xhr.upload.addEventListener('progress', ev => { if (ev.lengthComputable) { onProgress(Math.round(ev.loaded / ev.total * 100)); } }); xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { try { const resp = JSON.parse(xhr.responseText); if (resp.success) resolve(resp); else reject(new Error(resp.message || '伺服器錯誤')); } catch { reject(new Error('回應格式錯誤')); } } else { reject(new Error('HTTP ' + xhr.status)); } }); xhr.addEventListener('error', () => reject(new Error('網路錯誤'))); xhr.addEventListener('abort', () => reject(new Error('已取消'))); xhr.send(formData); }); } function updateProgress(index, pct) { const item = fileList.querySelector(`[data-index="${index}"]`); if (item) { item.querySelector('.fill').style.width = pct + '%'; item.querySelector('.status').textContent = pct + '%'; } } function updateStatus(index, text) { const item = fileList.querySelector(`[data-index="${index}"]`); if (item) item.querySelector('.status').textContent = text; } </script>

後端 PHP:upload.php(驗證、儲存、JSON 回應)

<?php
// upload.php
header('Content-Type: application/json; charset=utf-8');

// 設定
$allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
$maxSize = 10 * 1024 * 1024; // 10MB
$uploadDir = __DIR__ . '/uploads/';
$baseUrl = '/uploads/';

if (!is_dir($uploadDir)) {
    mkdir($uploadDir, 0755, true);
}

// 檢查請求
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    jsonOut(false, '僅允許 POST');
}
if (empty($_FILES['file'])) {
    jsonOut(false, '未收到檔案');
}

$file = $_FILES['file'];

// 錯誤代碼檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
    $msg = match ($file['error']) {
        UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => '檔案過大',
        UPLOAD_ERR_PARTIAL => '上傳中斷',
        UPLOAD_ERR_NO_FILE => '未選檔案',
        UPLOAD_ERR_NO_TMP_DIR => '伺服器暫存目錄缺失',
        UPLOAD_ERR_CANT_WRITE => '寫入失敗',
        UPLOAD_ERR_EXTENSION => '擴充套件阻擋',
        default => '未知錯誤',
    };
    jsonOut(false, $msg);
}

// 型別驗證(不信任瀏覽器傳來的 type,用 finfo 偵測)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowTypes, true)) {
    jsonOut(false, '檔案類型不允許:' . $mime);
}

// 大小驗證
if ($file['size'] > $maxSize) {
    jsonOut(false, '檔案超過 ' . ($maxSize/1024/1024) . 'MB');
}

// 產生安全檔名:隨機 + 原副檔名
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$safeName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = $uploadDir . $safeName;

// 移動檔案
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
    jsonOut(false, '儲存失敗');
}

// 可選:建立縮圖、掃毒、記錄資料庫...
// $db->prepare('INSERT INTO uploads (original, stored, mime, size) VALUES (?,?,?,?)')
//    ->execute([$file['name'], $safeName, $mime, $file['size']]);

jsonOut(true, '上傳成功', [
    'url' => $baseUrl . $safeName,
    'name' => $file['name'],
    'size' => $file['size'],
    'mime' => $mime,
]);

function jsonOut($success, $message, $data = null) {
    echo json_encode(['success' => $success, 'message' => $message, 'data' => $data], JSON_UNESCAPED_UNICODE);
    exit;
}

方案二:傳統 jquery.ajaxFileUpload(舊專案維護參考)

原理:動態建立隱藏 `