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

PHP + HTML5 FormData 實現無刷新檔案上傳完整教學 (原生 + jQuery + 進度條)

     編輯:PHP 綜合知識

HTML5 FormData + XMLHttpRequest/fetch 讓無刷新上傳變得極簡單,無需 iframe hack。本文涵蓋:FormData 建構三種方式、原生 XHR / jQuery / fetch 三種發送方式、進度條實作、PHP 端處理、多檔案、拖曳上傳、錯誤處理完整範例。

FormData 建構三種方式

// 1. 手動 append (最靈活,無需 form 標籤)
const fd = new FormData();
fd.append('name', 'value');
fd.append('file', document.getElementById('file').files[0]);
fd.append('files[]', ...document.getElementById('multi').files); // 多檔案

// 2. 從現有 form 表單建構 (自動包含所有 input/select/textarea)
const form = document.getElementById('myForm');
const fd = new FormData(form);
// 仍可追加額外欄位
fd.append('extra', 'value');

// 3. 從 input files 直接建構 (單一檔案欄位)
const fd = new FormData();
fd.append('avatar', document.querySelector('input[type=file]').files[0]);

發送方式對比

方式優點缺點關鍵設定
原生 XMLHttpRequest 支援 upload.onprogress 進度條、完整控制 代碼較多 無需設定 Content-Type,瀏覽器自動生成 multipart boundary
jQuery $.ajax 簡潔、統一錯誤處理 需 processData: false, contentType: false 同上
fetch API 現代 Promise、可搭配 Service Worker **無原生進度事件** (需 Stream 讀取 body 實作) 同上

完整範例:原生 XHR + 進度條 + PHP 端

HTML + JS

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>FormData Upload</title>
<style>
  .bar {width:300px;height:20px;background:#eee;border-radius:10px;overflow:hidden}
  .bar > div {height:100%;background:#4caf50;width:0;transition:width .3s}
</style></head><body>
<form id="upForm">
  <input type="file" id="file" name="file" required />
  <input type="text" name="title" placeholder="標題" />
  <button type="submit">上傳</button>
</form>
<div class="bar" id="progress" style="display:none"><div></div></div>
<div id="result"></div>

<script>
document.getElementById('upForm').addEventListener('submit', async e => {
  e.preventDefault();
  const fileInput = document.getElementById('file');
  if (!fileInput.files.length) return alert('請選擇檔案');

  const fd = new FormData(document.getElementById('upForm'));

  const progressBar = document.getElementById('progress');
  const progressInner = progressBar.querySelector('div');
  const resultDiv = document.getElementById('result');
  progressBar.style.display = 'block';
  progressInner.style.width = '0%';
  resultDiv.textContent = '';

  try {
    const resp = await uploadWithProgress('/upload.php', fd, (percent) => {
      progressInner.style.width = percent + '%';
    });
    resultDiv.innerHTML = '<pre style="color:green">' + JSON.stringify(resp, null, 2) + '</pre>';
  } catch (err) {
    resultDiv.innerHTML = '<pre style="color:red">' + err.message + '</pre>';
  } finally {
    progressBar.style.display = 'none';
  }
});

function uploadWithProgress(url, formData, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);

    xhr.upload.onprogress = e => {
      if (e.lengthComputable) {
        onProgress(Math.round((e.loaded / e.total) * 100));
      }
    };

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        try { resolve(JSON.parse(xhr.responseText)); }
        catch { resolve(xhr.responseText); }
      } else {
        reject(new Error(`HTTP ${xhr.status}: ${xhr.responseText}`));
      }
    };
    xhr.onerror = () => reject(new Error('Network error'));
    xhr.send(formData);
  });
}
</script>
</body></html>

PHP 端 upload.php

<?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'];
$maxSize = 10 * 1024 * 1024; // 10MB

$file = $_FILES['file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    echo json_encode(['error' => '上傳失敗: ' . ($file['error'] ?? '無檔案')]);
    exit;
}

if (!in_array($file['type'], $allowedTypes, true)) {
    http_response_code(400);
    echo json_encode(['error' => '不支援的檔案類型']);
    exit;
}

if ($file['size'] > $maxSize) {
    http_response_code(400);
    echo json_encode(['error' => '檔案過大 (限制 10MB)']);
    exit;
}

// 安全檔名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$dest = $uploadDir . $newName;

if (!move_uploaded_file($file['tmp_name'], $dest)) {
    http_response_code(500);
    echo json_encode(['error' => '儲存失敗']);
    exit;
}

echo json_encode([
    'success' => true,
    'file' => [
        'original' => $file['name'],
        'saved'    => $newName,
        'url'      => '/uploads/' . $newName,
        'size'     => $file['size'],
        'type'     => $file['type'],
    ],
    'fields' => $_POST, // title 等其他欄位
], JSON_UNESCAPED_UNICODE);
?>

jQuery 版本 (舊專案相容)

$('#upForm').on('submit', function(e) {
  e.preventDefault();
  const fd = new FormData(this);
  $.ajax({
    url: '/upload.php',
    type: 'POST',
    data: fd,
    processData: false,  // 關鍵:不要序列化 FormData
    contentType: false,  // 關鍵:讓瀏覽器自動設定 multipart boundary
    xhr: () => {
      const xhr = new XMLHttpRequest();
      xhr.upload.onprogress = e => {
        if (e.lengthComputable) {
          $('#progress div').css('width', Math.round(e.loaded/e.total*100)+'%');
        }
      };
      return xhr;
    },
  }).done(res => $('#result').html('
'+JSON.stringify(res,null,2)+'
')) .fail(err => $('#result').html('
'+err.responseText+'
')); });

拖曳上傳擴充 (5 行代碼)

const dropZone = document.getElementById('dropZone');
['dragenter','dragover'].forEach(evt => dropZone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('active'); }));
['dragleave','drop'].forEach(evt => dropZone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('active'); }));
dropZone.addEventListener('drop', e => {
  document.getElementById('file').files = e.dataTransfer.files; // 同步到隱藏 input
  document.getElementById('upForm').dispatchEvent(new Event('submit')); // 觸發上傳
});

常見坑點

  • $_FILES 為空 → 檢查 enctype="multipart/form-data"、contentType: false、Nginx client_max_body_size、PHP post_max_size/upload_max_filesize。
  • 進度條不動 → 確認 xhr.upload.onprogress 綁定在 send() 之前、伺服器未啟用緩衝 (Nginx proxy_request_buffering off)。
  • 大檔案斷點續傳 → FormData 無法直接支援,需前端切片 (Blob.slice) + 後端合併,建議用 Uppy / tus.io 協議。

總結

FormData 是現代無刷新上傳標準。**原生 XHR** 適合需進度條、**fetch** 適合簡單火後不理、**jQuery** 適合舊專案。PHP 端務必驗證型別、大小、副檔名、用隨機檔名儲存、回傳 JSON。拖曳上傳只需監聽 drop 事件同步檔案到隱藏 <input type=file> 即可複用現有表單邏輯。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved