AJAX(Asynchronous JavaScript and XML)雖非新技術,但構建現代 Web 應用的基石。本文從原理到實戰,系統講解原生 JS 實現 AJAX、GET/POST 請求處理、文件上傳偽 AJAX 方案,並提供可直接落地的 PHP 後端代碼。
核心對象:XMLHttpRequest (XHR),允許瀏覽器在無刷新情況下與服務器異步交換數據。
創建 XHR → 綁定 onreadystatechange 回調 → open(method, url, async) → send(data) → 服務器處理 → 回調處理響應
<?php
// 文件:assets/js/ajax.js
/**
* 跨瀏覽器創建 XMLHttpRequest 對象
*/
function createXHR() {
var xhr = null;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest(); // 現代瀏覽器
} else if (window.ActiveXObject) {
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP'); // IE6+
} catch (e) {
xhr = new ActiveXObject('Microsoft.XMLHTTP'); // IE5.5-
}
}
return xhr;
}
/**
* 通用 AJAX 請求封裝
* @param {Object} options {method, url, data, success, error}
*/
function ajax(options) {
var xhr = createXHR();
if (!xhr) return false;
var method = (options.method || 'GET').toUpperCase();
var url = options.url;
var data = options.data || null;
var async = options.async !== false;
// GET 請求參數拼接到 URL
if (method === 'GET' && data) {
var params = [];
for (var k in data) {
params.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
}
url += (url.indexOf('?') > -1 ? '&' : '?') + params.join('&');
data = null;
}
xhr.open(method, url, async);
// POST 需設置 Content-Type
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// 對象轉字符串
if (typeof data === 'object') {
var params = [];
for (var k in data) {
params.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
}
data = params.join('&');
}
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
options.success && options.success(xhr.responseText, xhr);
} else {
options.error && options.error(xhr.status, xhr);
}
}
};
xhr.send(data);
return xhr;
}
?>
<!-- HTML -->
<a href="javascript:;" onclick="loadNews(1)">查看新聞 1</a>
<div id="news-detail"></div>
<script>
function loadNews(id) {
ajax({
method: 'GET',
url: '/api/news.php',
data: { id: id },
success: function(response) {
document.getElementById('news-detail').innerHTML = response;
},
error: function(status) {
alert('加載失敗:' + status);
}
});
}
</script>
<?php
// api/news.php
header('Content-Type: text/html; charset=utf-8');
// 模擬數據庫查詢
$newsData = [
1 => ['title' => 'PHP 8.1 發布', 'content' => '帶來 Enum、Fiber 等新特性...'],
2 => ['title' => 'Composer 2.0 加速', 'content' => '安裝速度提升 50%...'],
];
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (isset($newsData[$id])) {
echo "<h3>{$newsData[$id]['title']}</h3>";
echo "<p>{$newsData[$id]['content']}</p>";
} else {
http_response_code(404);
echo '新聞不存在';
}
?>
<form id="user-form">
<input type="text" name="username" placeholder="用戶名" required>
<input type="email" name="email" placeholder="郵箱" required>
<button type="submit">提交</button>
</form>
<div id="form-msg"></div>
<script>
document.getElementById('user-form').addEventListener('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
var data = {};
formData.forEach(function(v, k) { data[k] = v; });
ajax({
method: 'POST',
url: '/api/user_save.php',
data: data,
success: function(res) {
document.getElementById('form-msg').innerHTML = '<span style="color:green">' + res + '</span>';
},
error: function() {
document.getElementById('form-msg').innerHTML = '<span style="color:red">提交失敗</span>';
}
});
});
</script>
<?php
// api/user_save.php
header('Content-Type: application/json; charset=utf-8');
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$username = trim($input['username'] ?? '');
$email = trim($input['email'] ?? '');
if (!$username || !$email) {
http_response_code(400);
echo json_encode(['code' => 400, 'msg' => '參數缺失']);
exit;
}
// 這裡接入數據庫操作(建議使用 PDO 預處理)
// $pdo->prepare('INSERT INTO users (username, email) VALUES (?, ?)')->execute([$username, $email]);
echo json_encode(['code' => 200, 'msg' => '用戶 ' . htmlspecialchars($username) . ' 保存成功']);
?>
早期 XHR 不支持文件上傳,現代項目請直接使用 FormData + XHR2 或 fetch API。此處僅作歷史方案記錄。
表單 target 指向隱藏 iframe → 提交後服務器返回 JS 腳本 → 調用父頁面回調函數顯示結果
<iframe name="upload_iframe" id="upload_iframe" style="display:none;"></iframe>
<form action="/api/upload.php" method="post" enctype="multipart/form-data" target="upload_iframe">
<input type="file" name="image" required>
<button type="submit">上傳</button>
</form>
<div id="upload-msg"></div>
<script>
// 父頁面回調函數
window.uploadCallback = function(msg, success) {
var el = document.getElementById('upload-msg');
el.innerHTML = msg;
el.style.color = success ? 'green' : 'red';
};
</script>
<?php
// api/upload.php
$allowedMime = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 2 * 1024 * 1024; // 2MB
$uploadDir = __DIR__ . '/../uploads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$msg = '上傳失敗';
$success = false;
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$file = $_FILES['image'];
// 驗證類型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedMime)) {
$msg = '只允許上傳 JPG/PNG/GIF/WebP 圖片';
} elseif ($file['size'] > $maxSize) {
$msg = '文件大小不能超過 2MB';
} else {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = uniqid('img_') . '.' . $ext;
$dest = $uploadDir . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$msg = '上傳成功:' . $newName;
$success = true;
} else {
$msg = '文件移動失敗,請檢查目錄權限';
}
}
} else {
$msg = '未選擇文件或上傳出錯(錯誤碼:' . ($_FILES['image']['error'] ?? '未知') . ')';
}
// 關鍵:輸出 JS 腳本調用父頁面回調
echo "<script>";
echo "parent.uploadCallback('" . addslashes($msg) . "', " . ($success ? 'true' : 'false') . ");";
echo "</script>";
?>
// 現代寫法(推薦)
fetch('/api/user_save.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username: '張三', email: 'zhang@example.com'})
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
htmlspecialchars() 或 json_encode() 防 XSSCache-Control、ETag){code, msg, data}掌握 AJAX 核心機制,配合現代前端工具鏈,能讓 PHP 後端輕松支撐流暢的單頁應用體驗。