「無刷新上傳」本質是不導航當前頁面完成文件提交並獲取響應。歷史上主流有兩種實現:iframe 表單提交(兼容性極佳,IE6+)與 AJAX (XMLHttpRequest/Fetch) + FormData(現代標準,支持進度條)。本文提供兩套完整可運行代碼,並分析優缺點與適用場景。
<iframe name="uploader" style="display:none">。<form target="uploader"> 指向 iframe,提交後頁面不跳轉。<script>parent.callback(data)</script>,調用父頁面回調。<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>iframe 上傳演示</title></head>
<body>
<h3>iframe 無刷新上傳</h3>
<iframe name="uploader" style="display:none;"></iframe>
<form action="upload_iframe.php" method="post" enctype="multipart/form-data" target="uploader">
<input type="file" name="upfile" required>
<button type="submit">上傳</button>
</form>
<div id="msg" style="margin-top:10px;color:green"></div>
<script>
function uploadCallback(res) {
document.getElementById('msg').textContent = res.success ? '上傳成功:' + res.url : '失敗:' + res.error;
}
</script>
</body></html>
<?php
header('Content-Type: text/html; charset=utf-8');
$targetDir = __DIR__ . '/uploads/';
if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
$resp = ['success' => false];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upfile'])) {
$file = $_FILES['upfile'];
if ($file['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = uniqid('img_') . '.' . $ext;
$dest = $targetDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$resp = ['success' => true, 'url' => '/uploads/' . $newName];
} else {
$resp['error'] = '移動文件失敗';
}
} else {
$resp['error'] = '上傳錯誤碼:' . $file['error'];
}
}
?>
<script>
window.parent.uploadCallback(= json_encode($resp, JSON_UNESCAPED_UNICODE) ?>);
</script>
| 優點 | 缺點 |
|---|---|
| 兼容所有瀏覽器(含 IE6) | 無法獲取上傳進度 |
| 代碼極簡,無需 JS 庫 | 需服務端輸出 JS 腳本,耦合度高 |
| 天然支持跨域(同域策略下) | 錯誤處理困難,難以區分網絡錯誤與業務錯誤 |
| 可在舊項目零成本接入 | 響應需嵌入 <script>,易發生 XSS 若未轉義 |
FormData 自動構造 multipart/form-data 體。XMLHttpRequest.upload.onprogress 或 fetch 配合 ReadableStream 實時進度。<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>AJAX 上傳演示</title></head>
<body>
<h3>AJAX 無刷新上傳(帶進度)</h3>
<input type="file" id="fileInput">
<button id="btnUpload">上傳</button>
<progress id="progress" value="0" max="100" style="width:300px"></progress>
<div id="result"></div>
<script>
document.getElementById('btnUpload').onclick = async () => {
const file = document.getElementById('fileInput').files[0];
if (!file) return alert('請選擇文件');
const formData = new FormData();
formData.append('upfile', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', 'upload_ajax.php', true);
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
document.getElementById('progress').value = (e.loaded / e.total) * 100;
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const res = JSON.parse(xhr.responseText);
document.getElementById('result').textContent = res.success ? '成功:' + res.url : '失敗:' + res.error;
} else {
document.getElementById('result').textContent = 'HTTP ' + xhr.status;
}
};
xhr.onerror = () => document.getElementById('result').textContent = '網絡錯誤';
xhr.send(formData);
};
</script>
</body></html>
<?php
header('Content-Type: application/json; charset=utf-8');
$targetDir = __DIR__ . '/uploads/';
if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
$resp = ['success' => false];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['upfile'])) {
$file = $_FILES['upfile'];
if ($file['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = uniqid('img_') . '.' . $ext;
$dest = $targetDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$resp = ['success' => true, 'url' => '/uploads/' . $newName];
} else {
$resp['error'] = '移動文件失敗';
}
} else {
$resp['error'] = '上傳錯誤碼:' . $file['error'];
}
}
echo json_encode($resp, JSON_UNESCAPED_UNICODE);
const res = await fetch('upload_ajax.php', {
method: 'POST',
body: formData
// 不要手動設置 Content-Type,瀏覽器會自動帶上 boundary
});
const data = await res.json();
$_FILES['upfile']['type']、擴展名、finfo_file 魔數。php.ini upload_max_filesize + 前端 file.size 雙重校驗。{code, message, data} 結構。| 場景 | 推薦方案 |
|---|---|
| 須支持 IE8/9、老舊內網系統 | iframe |
| 需要上傳進度條、大文件分片、斷點續傳 | AJAX + FormData / WebUploader / Uppy |
| 現代項目、前後端分離、TypeScript/React/Vue | AJAX (Fetch) + 統一請求庫 |
| 快速原型、零依賴、內部工具 | iframe 可接受 |
iframe 方案是「兼容性兜底」,AJAX + FormData 是「現代標準」。新項目默認選 AJAX,享受進度監控、類型安全、前後端解耦帶來的開發體驗提升;僅在不得不兼容古老環境時才退回 iframe。