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

PHP + AJAX 異步文件上傳:從 iframe 方案到現代 Fetch/XHR2 完整演進

     編輯:PHP 相關知識

歷史背景與技術演進

時代技術方案核心原理缺點
~2010隱藏 <iframe> + Form 提交表單 target 指向 iframe,服務端返回 JS 腳本操作父頁面 DOM體驗差、調試難、無進度、跨域受限
2010-2015Flash/Silverlight 上傳組件插件提供文件系統訪問、多文件、進度條需插件、移動端不支持、安全漏洞多
2012+XMLHttpRequest Level 2 + FormData原生支持二進制、進度事件、跨域(CORS)IE9 以下不支持
2015+Fetch API + FormDataPromise 化、流式處理、Service Worker 友好無原生進度事件、需 polyfill
2020+現代 Uploader 庫分片上傳、斷點續傳、並發控制、預簽名直傳 OSS引入依賴、配置複雜

方案一:現代原生 Fetch + FormData(推薦新項目)

前端核心代碼

<!-- HTML -->
<input type="file" id="fileInput" multiple accept="image/*,application/pdf">
<button id="uploadBtn">上傳</button>
<div id="progress" style="display:none;">
    <progress id="progressBar" max="100" value="0"></progress>
    <span id="progressText">0%</span>
</div>
<div id="fileList"></div>

<script>
document.getElementById('uploadBtn').addEventListener('click', async () => {
    const files = document.getElementById('fileInput').files;
    if (!files.length) return alert('請選擇文件');
    
    const formData = new FormData();
    for (const file of files) {
        formData.append('files[]', file);
    }
    
    const progressBar = document.getElementById('progressBar');
    const progressText = document.getElementById('progressText');
    const fileList = document.getElementById('fileList');
    document.getElementById('progress').style.display = 'block';
    
    try {
        // 使用 XMLHttpRequest 獲取上傳進度(Fetch 目前無原生進度)
        const response = await uploadWithProgress('/api/upload.php', formData, (percent) => {
            progressBar.value = percent;
            progressText.textContent = `${percent}%`;
        });
        
        const result = await response.json();
        if (result.success) {
            fileList.innerHTML = result.data.map(f => 
                `
✅ ${f.name} (${(f.size/1024).toFixed(1)} KB) - 查看
` ).join(''); } else { throw new Error(result.message); } } catch (err) { fileList.innerHTML = `
❌ ${err.message}
`; } finally { document.getElementById('progress').style.display = 'none'; } }); // 封裝:XHR 上傳 + 進度回調 + 返回 Promise function uploadWithProgress(url, formData, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url); // 上傳進度事件 xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { onProgress(Math.round((e.loaded / e.total) * 100)); } }); xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { resolve({ json: () => Promise.resolve(JSON.parse(xhr.responseText)) }); } else { reject(new Error(`HTTP ${xhr.status}`)); } }); xhr.addEventListener('error', () => reject(new Error('網絡錯誤'))); xhr.addEventListener('abort', () => reject(new Error('已取消'))); xhr.send(formData); }); } </script>

後端 PHP 處理(支持多文件、驗證、安全存儲)

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

// 配置
$config = [
    'upload_dir'   => __DIR__ . '/../storage/uploads/',
    'allowed_mime' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'],
    'max_size'     => 5 * 1024 * 1024, // 5MB
    'url_prefix'   => '/storage/uploads/',
];

// 確保目錄存在
if (!is_dir($config['upload_dir'])) {
    mkdir($config['upload_dir'], 0755, true);
}

$results = [];
$errors = [];

foreach ($_FILES['files']['tmp_name'] as $idx => $tmpName) {
    $error = $_FILES['files']['error'][$idx];
    if ($error !== UPLOAD_ERR_OK) {
        $errors[] = "文件 {$_FILES['files']['name'][$idx]}: " . uploadErrorMsg($error);
        continue;
    }
    
    $name     = $_FILES['files']['name'][$idx];
    $size     = $_FILES['files']['size'][$idx];
    $mime     = mime_content_type($tmpName); // 真實 MIME,不可信 $_FILES['type']
    $ext      = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    
    // 驗證
    if (!in_array($mime, $config['allowed_mime'], true)) {
        $errors[] = "{$name}: 類型不允許 ({$mime})";
        continue;
    }
    if ($size > $config['max_size']) {
        $errors[] = "{$name}: 超過大小限制";
        continue;
    }
    
    // 生成安全文件名
    $safeName = bin2hex(random_bytes(16)) . ".{$ext}";
    $destPath = $config['upload_dir'] . $safeName;
    
    if (!move_uploaded_file($tmpName, $destPath)) {
        $errors[] = "{$name}: 移動失敗";
        continue;
    }
    
    // 設置權限
    chmod($destPath, 0644);
    
    $results[] = [
        'name' => $name,
        'size' => $size,
        'mime' => $mime,
        'url'  => $config['url_prefix'] . $safeName,
        'path' => $destPath,
    ];
}

echo json_encode([
    'success' => empty($errors),
    'message' => $errors ? implode('; ', $errors) : '上傳成功',
    'data'    => $results,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

function uploadErrorMsg(int $code): string {
    return match($code) {
        UPLOAD_ERR_INI_SIZE   => '超過 php.ini upload_max_filesize',
        UPLOAD_ERR_FORM_SIZE  => '超過表單 MAX_FILE_SIZE',
        UPLOAD_ERR_PARTIAL    => '僅部分上傳',
        UPLOAD_ERR_NO_FILE    => '無文件',
        UPLOAD_ERR_NO_TMP_DIR => '缺少臨時目錄',
        UPLOAD_ERR_CANT_WRITE => '寫入磁盤失敗',
        UPLOAD_ERR_EXTENSION  => '被擴展阻止',
        default               => '未知錯誤',
    };
}

方案二:大文件分片上傳(斷點續傳、秒傳)

核心流程

  1. 前端:文件分片(如 5MB/片)→ 計算全文件 hash (SparkMD5) → 檢查服務端是否已存在 → 並發上傳分片
  2. 服務端:接收分片 → 臨時目錄存儲 → 全部分片到齊 → 合併 → 驗證 hash → 移動到正式目錄
  3. 關鍵接口:
    • POST /api/upload/check:檢查文件/分片是否存在(秒傳/斷點續傳)
    • POST /api/upload/chunk:上傳單個分片
    • POST /api/upload/merge:合併分片

推薦成熟庫(避免重複造輪子)

  • Uppy:模塊化、React/Vue/Svelte 組件、支持 Tus/HTTP/S3 多協議
  • FilePond:輕量、美觀、圖片預覽/編輯、插件化
  • Dropzone.js:經典、拖拽上傻、進度、縮略圖
  • Plupload:支持分片、多運行時、較老但穩定
  • Tus.io:開放協議、標準化斷點續傳、多語言服務端實現

安全加固清單(無論哪種方案)

層面措施
文件驗證MIME 白名單 + 文件頭魔數檢測 + 副檢查副檔名
存儲安全隨機文件名 + 非 Web 可訪問目錄 + Nginx 禁用執行 + 防盜鏈
大小限制前端 + PHP(upload_max_filesize/post_max_size) + Nginx(client_max_body_size) 三層
權限控制登錄態驗證 + 業務權限(如僅作者可上傳封面)+ CSRF Token
病毒掃描ClamAV 異步掃描、可疑文件隔離
日誌審計記錄用戶、IP、文件信息、操作結果,保留 6 個月

選型建議:小項目用原生 Fetch + FormData(零依賴、現代瀏覽器全支持);中型項目引入 Uppy/FilePond(體驗好、維護省);大文件/斷點續傳必須用分片上傳(Tus 協議或 Uppy Tus 插件);生產環境務必做三層大小限制 + MIME 雙重驗證 + 存儲目錄隔離。

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