HTML5 FormData + XMLHttpRequest Level 2 出現前,無刷新上傳主流做法是 隱藏 iframe 當作 form target。本文完整保留此經典實作,並補充現代化替代方案。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>AJAX 檔案上傳示例</title>
<style>
#container { width: 400px; margin: 30px auto; border: 1px solid #003; background: #fff; font-family: sans-serif; }
#header { height: 42px; background: url(images/header_bg.gif) repeat-x; color: #fff; padding: 5px; font-weight: bold; }
#content { padding: 15px; color: #666; }
#footer { text-align: right; padding: 5px 10px; border-top: 1px solid #999; color: #999; font-size: 12px; }
.msg { background: #e8f5e9; color: #2e7d32; padding: 10px; margin: 10px; border-radius: 4px; }
.emsg { background: #fbe9e7; color: #c62828; padding: 10px; margin: 10px; border-radius: 4px; }
#loader { display: none; text-align: center; padding: 20px; }
.sbtn { background: #003; color: #fff; border: 1px solid #003; padding: 5px 20px; cursor: pointer; }
#f1_upload_process { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; background: #fff; border: 1px solid #ccc; padding: 20px; display: none; z-index: 100; box-shadow: 0 2px 10px rgba(0,0,0,.2); }
</style>
</head>
<body>
<div id="container">
<div id="header">AJAX 檔案上傳示例</div>
<div id="content">
<form action="upload.php" method="post" enctype="multipart/form-data" target="upload_target" onsubmit="startUpload();">
<div id="f1_upload_process">上傳中...<br><img src="loader.gif" alt="loading"></div>
<div id="f1_upload_form">
<label>選擇檔案: <input name="myfile" type="file" size="30" required></label>
<label><input type="submit" class="sbtn" value="上傳"></label>
</div>
<iframe id="upload_target" name="upload_target" src="#" style="width:0;height:0;border:0;display:none;"></iframe>
</form>
</div>
<div id="footer">Powered by PHP + iframe</div>
</div>
<script>
function startUpload() {
document.getElementById('f1_upload_process').style.display = 'block';
document.getElementById('f1_upload_form').style.display = 'none';
return true;
}
function stopUpload(success) {
var result = '';
if (success == 1) {
result = '<div class="msg">檔案上傳成功!</div><br>';
} else {
result = '<div class="emsg">上傳失敗,請重試。</div><br>';
}
document.getElementById('f1_upload_process').style.display = 'none';
document.getElementById('f1_upload_form').innerHTML = result +
'<label>選擇檔案: <input name="myfile" type="file" size="30" required></label>' +
'<label><input type="submit" class="sbtn" value="上傳"></label>';
document.getElementById('f1_upload_form').style.display = 'block';
return true;
}
</script>
</body>
</html>
<?php
// upload.php
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$maxSize = 5 * 1024 * 1024; // 5MB
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$success = 0;
$msg = '';
if (isset($_FILES['myfile'])) {
$file = $_FILES['myfile'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$msg = '上傳錯誤代碼: ' . $file['error'];
} elseif (!in_array($file['type'], $allowedTypes)) {
$msg = '不允許的檔案類型';
} elseif ($file['size'] > $maxSize) {
$msg = '檔案過大,限制 5MB';
} else {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = date('YmdHis') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$dest = $uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$success = 1;
$msg = '上傳成功:' . $newName;
} else {
$msg = '移動檔案失敗';
}
}
} else {
$msg = '未收到檔案';
}
// 輸出 JavaScript 呼叫父視窗 stopUpload()
?>
<script>
window.top.window.stopUpload();
</script>
| 環節 | 說明 |
|---|---|
| form target="upload_target" | 表單提交後,回應在隱藏 iframe 中載入,不刷新主頁 |
| onsubmit="startUpload()" | 顯示載入動畫、隱藏表單 |
| upload.php 結尾輸出 JS | window.top.window.stopUpload(1) 呼叫主頁函數處理結果 |
| stopUpload() 重建表單 | 用 innerHTML 重置表單,支援連續上傳 |
<script>
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const btn = e.target.querySelector('button');
btn.disabled = true; btn.textContent = '上傳中...';
try {
const res = await fetch('upload.php', { method: 'POST', body: formData });
const text = await res.text();
alert(text); // 或解析 JSON
} catch (err) {
alert('網路錯誤');
} finally {
btn.disabled = false; btn.textContent = '上傳';
}
});
</script>
優點:原生 Promise、支援 xhr.upload.onprogress 真實進度條、無需 iframe、程式碼更簡潔。
iframe 上傳是經典跨瀏覽器相容方案,適合維護舊專案。新專案請直接使用 Fetch API + FormData,配合後端回傳 JSON,體驗更佳且易於擴展進度條、分塊上傳、暫停續傳等進階功能。