Ajax 核心是 XMLHttpRequest (XHR) 物件,實現無刷新數據交互。本文從 XHR 創建、請求流程、PHP 端處理、同源策略、錯誤處理、現代 fetch() 替代方案全維度講解。
function createXHR(): XMLHttpRequest {
if (window.XMLHttpRequest) {
return new XMLHttpRequest(); // 現代瀏覽器、IE7+
}
// IE5/6 兼容(已基本無需)
try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) {}
try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {}
throw new Error('XMLHttpRequest not supported');
}
const xhr = createXHR();
const url = 'api.php?action=get_user&id=123';
xhr.open('GET', url, true); // true = 異步
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) { // 完成
if (xhr.status >= 200 && xhr.status < 300) {
const response = xhr.responseText;
try {
const data = JSON.parse(response);
console.log('成功:', data);
// 更新 DOM
document.getElementById('result').textContent = data.name;
} catch (e) {
console.error('JSON 解析失敗', response);
}
} else {
console.error('HTTP 錯誤:', xhr.status, xhr.statusText);
}
}
};
// 可選:超時處理
xhr.timeout = 5000;
xhr.ontimeout = () => console.error('請求超時');
xhr.send(null); // GET 請求體為 null
const formData = new FormData();
formData.append('username', 'zhangsan');
formData.append('password', 'secret123');
xhr.open('POST', 'login.php', true);
// 發送 FormData 會自動設置 Content-Type: multipart/form-data
xhr.send(formData);
// 或發送 JSON
xhr.open('POST', 'api.php', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({action: 'save', data: {...}}));
<?php
// api.php
header('Content-Type: application/json; charset=utf-8');
// 統一輸入獲取
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST ?? $_GET;
$action = $input['action'] ?? '';
try {
switch ($action) {
case 'get_user':
$id = (int)($input['id'] ?? 0);
$user = $db->query("SELECT id, name, email FROM users WHERE id=$id")->fetch(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'data' => $user]);
break;
case 'save':
// 驗證、CSRF 檢查、業務邏輯
echo json_encode(['success' => true, 'msg' => '保存成功']);
break;
default:
http_response_code(400);
echo json_encode(['success' => false, 'msg' => '未知 action']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'msg' => '服務器錯誤']);
}
Access-Control-Allow-Origin: * 或指定域名;proxy_pass 轉發到目標 API;callback,已過時。<?php
// CORS 頭(放在最前)
header('Access-Control-Allow-Origin: https://your-frontend.com');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Requested-With');
header('Access-Control-Allow-Credentials: true'); // 攜帶 Cookie
// 預檢請求快速返回
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// GET
fetch('/api.php?action=get_user&id=123')
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
.then(data => console.log(data))
.catch(err => console.error(err));
// POST JSON
fetch('/api.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({action: 'save', data: {name: 'test'}}),
credentials: 'include' // 攜帶 Cookie
}).then(r => r.json()).then(console.log);
// 取消請求
const controller = new AbortController();
fetch('/api.php', {signal: controller.signal});
setTimeout(() => controller.abort(), 5000);
fetch,老項目 XHR 共存。| 問題 | 原因 | 修正 |
|---|---|---|
| readyState 4 但 status 0 | 跨域被攔截 / 網絡斷開 | 檢查 CORS、網絡面板 |
| JSON.parse 報錯 | PHP 輸出了錯誤信息/空白/警告 | 關閉 display_errors、ob_clean()、統一 json_encode |
| POST 參數為空 | Content-Type 不匹配 / php://input 已讀 | 檢查 setRequestHeader、僅讀取一次 |
| 攜帶 Cookie 失效 | 跨域需 credentials: true + CORS Allow-Credentials | 雙端同步設置 |
| 重複請求 | 按鈕未禁用、事件冒泡 | loading 狀態鎖、防抖 |
function ajax(options) {
const {url, method='GET', data, headers={}, timeout=10000, onProgress} = options;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.timeout = timeout;
Object.entries(headers).forEach(([k,v]) => xhr.setRequestHeader(k, v));
if (onProgress) xhr.upload.onprogress = onProgress;
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try { resolve(JSON.parse(xhr.responseText)); }
catch { resolve(xhr.responseText); }
} else { reject(new Error(`${xhr.status} ${xhr.statusText}`)); }
};
xhr.onerror = () => reject(new Error('Network Error'));
xhr.ontimeout = () => reject(new Error('Timeout'));
const body = data instanceof FormData ? data : (data ? JSON.stringify(data) : null);
if (body && !headers['Content-Type']) {
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.send(body);
});
}
// 使用
ajax({url: '/api.php', method: 'POST', data: {action: 'save'}})
.then(data => console.log(data))
.catch(err => alert(err.message));
理解 XHR 底層機制能更好地調試網絡問題,現代項目建議直接使用 fetch 或 Axios 等成熟庫。