XMLHttpRequest:AJAX 的起源與原理
在 Fetch API 出現之前,XMLHttpRequest(XHR)是瀏覽器中發送 HTTP 請求的唯一方式。它是 AJAX 技術的基石,改變了 Web 應用的開發方式。
一、 歷史背景
1.1 AJAX 的誕生
AJAX = Asynchronous JavaScript And XML
2005 年,Jesse James Garrett 發表文章《Ajax: A New Approach to Web Applications》,正式命名了這種技術模式。
傳統 vs AJAX:
1.2 XMLHttpRequest 的起源
1999 年,Microsoft 在 IE5 中引入 XMLHTTP ActiveX 控制項。
javascript
// IE5/6 時代
var xhr = new ActiveXObject("Microsoft.XMLHTTP");2006 年,W3C 將其標準化為 XMLHttpRequest:
javascript
// 現代標準
var xhr = new XMLHttpRequest();NOTE
雖然名稱包含 XML,但 XHR 可以處理任何格式的資料,包括 JSON、HTML、純文字等。
二、 基本用法
2.1 發送 GET 請求
javascript
const xhr = new XMLHttpRequest();
// 設定請求
xhr.open("GET", "https://api.example.com/users", true);
// 監聽狀態變化
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
// 發送請求
xhr.send();2.2 發送 POST 請求
javascript
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://api.example.com/users", true);
// 設定 Content-Type
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
const response = JSON.parse(xhr.responseText);
console.log("創建成功:", response);
} else {
console.error("請求失敗:", xhr.status);
}
}
};
// 發送 JSON 資料
xhr.send(
JSON.stringify({
name: "John",
email: "john@example.com",
})
);三、 readyState 狀態
XHR 有 5 種狀態:
| readyState | 常量 | 說明 |
|---|---|---|
| 0 | UNSENT | 尚未呼叫 open() |
| 1 | OPENED | 已呼叫 open() |
| 2 | HEADERS_RECEIVED | 收到回應標頭 |
| 3 | LOADING | 正在接收 Body |
| 4 | DONE | 完成 |
3.1 狀態變化範例
javascript
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
console.log("readyState:", xhr.readyState);
switch (xhr.readyState) {
case 0:
console.log("UNSENT");
break;
case 1:
console.log("OPENED");
break;
case 2:
console.log("HEADERS_RECEIVED");
break;
case 3:
console.log("LOADING");
break;
case 4:
console.log("DONE");
break;
}
};
xhr.open("GET", "/api/data", true);
xhr.send();
// 輸出:
// readyState: 1 (OPENED)
// readyState: 2 (HEADERS_RECEIVED)
// readyState: 3 (LOADING)
// readyState: 4 (DONE)四、 事件模型
4.1 進度事件
XHR Level 2 引入了更豐富的事件:
| 事件 | 說明 |
|---|---|
loadstart | 請求開始 |
progress | 傳輸進度 |
load | 請求成功完成 |
error | 請求失敗 |
abort | 請求被取消 |
timeout | 請求超時 |
loadend | 請求結束(無論成敗) |
4.2 使用事件監聽
javascript
const xhr = new XMLHttpRequest();
xhr.addEventListener("loadstart", () => {
console.log("請求開始");
});
xhr.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
console.log(`進度: ${percent.toFixed(1)}%`);
}
});
xhr.addEventListener("load", () => {
console.log("請求完成:", xhr.status);
});
xhr.addEventListener("error", () => {
console.error("網路錯誤");
});
xhr.addEventListener("timeout", () => {
console.error("請求超時");
});
xhr.addEventListener("loadend", () => {
console.log("請求結束");
});
xhr.open("GET", "/api/large-file", true);
xhr.send();4.3 上傳進度
javascript
const xhr = new XMLHttpRequest();
// 上傳進度
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
console.log(`上傳進度: ${percent.toFixed(1)}%`);
}
});
xhr.open("POST", "/api/upload", true);
xhr.send(formData);五、 常用 API
5.1 設定請求
javascript
const xhr = new XMLHttpRequest();
// 設定方法、URL、是否非同步
xhr.open("GET", "/api/data", true);
// 設定請求標頭
xhr.setRequestHeader("Content-Type", "application/json");
xhr.setRequestHeader("Authorization", "Bearer token123");
// 設定超時(毫秒)
xhr.timeout = 30000;
// 設定回應類型
xhr.responseType = "json"; // 'text', 'document', 'blob', 'arraybuffer'
// 攜帶 Cookie(跨域時)
xhr.withCredentials = true;5.2 讀取回應
javascript
xhr.onload = function () {
// 狀態碼
console.log(xhr.status); // 200
console.log(xhr.statusText); // "OK"
// 回應標頭
console.log(xhr.getResponseHeader("Content-Type"));
console.log(xhr.getAllResponseHeaders());
// 回應內容
console.log(xhr.responseText); // 文字
console.log(xhr.response); // 根據 responseType
console.log(xhr.responseXML); // XML Document
};5.3 取消請求
javascript
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data", true);
xhr.send();
// 取消請求
xhr.abort();六、 responseType 詳解
6.1 類型選項
| 值 | 說明 | response 類型 |
|---|---|---|
'' (空) | 預設,視為 text | string |
'text' | 純文字 | string |
'json' | JSON,自動解析 | object |
'document' | HTML/XML 文件 | Document |
'blob' | 二進位大物件 | Blob |
'arraybuffer' | 二進位陣列 | ArrayBuffer |
6.2 使用範例
javascript
// JSON
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/users", true);
xhr.responseType = "json";
xhr.onload = () => {
console.log(xhr.response); // 已經是物件,不需 JSON.parse
};
xhr.send();
// 下載檔案
const xhr2 = new XMLHttpRequest();
xhr2.open("GET", "/files/document.pdf", true);
xhr2.responseType = "blob";
xhr2.onload = () => {
const blob = xhr2.response;
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.pdf";
a.click();
};
xhr2.send();七、 錯誤處理
7.1 區分錯誤類型
javascript
const xhr = new XMLHttpRequest();
xhr.onload = function () {
// HTTP 錯誤(收到回應,但狀態碼表示錯誤)
if (xhr.status >= 400) {
console.error("HTTP 錯誤:", xhr.status);
} else {
console.log("成功:", xhr.response);
}
};
xhr.onerror = function () {
// 網路錯誤(無法連線)
console.error("網路錯誤");
};
xhr.ontimeout = function () {
// 超時
console.error("請求超時");
};
xhr.onabort = function () {
// 被取消
console.log("請求已取消");
};
xhr.open("GET", "/api/data", true);
xhr.timeout = 5000;
xhr.send();7.2 封裝 Promise
javascript
function request(method, url, data = null, options = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
// 設定標頭
if (options.headers) {
Object.entries(options.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
}
// 設定超時
if (options.timeout) {
xhr.timeout = options.timeout;
}
// 設定回應類型
xhr.responseType = options.responseType || "json";
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error("Network error"));
xhr.ontimeout = () => reject(new Error("Timeout"));
xhr.onabort = () => reject(new Error("Aborted"));
// 發送
if (data && typeof data === "object") {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(data));
} else {
xhr.send(data);
}
});
}
// 使用
async function fetchUsers() {
try {
const users = await request("GET", "/api/users");
console.log(users);
} catch (error) {
console.error(error.message);
}
}八、 跨域請求(CORS)
8.1 簡單請求
javascript
const xhr = new XMLHttpRequest();
xhr.open("GET", "https://api.other-domain.com/data", true);
xhr.send(); // 瀏覽器自動加 Origin 標頭8.2 攜帶憑證
javascript
const xhr = new XMLHttpRequest();
// 允許跨域攜帶 Cookie
xhr.withCredentials = true;
xhr.open("GET", "https://api.other-domain.com/user", true);
xhr.send();伺服器必須回應:
http
Access-Control-Allow-Origin: https://your-domain.com
Access-Control-Allow-Credentials: true九、 同步 vs 非同步
9.1 非同步請求(推薦)
javascript
xhr.open("GET", "/api/data", true); // 第三參數 true = 非同步9.2 同步請求(已過時)
javascript
xhr.open("GET", "/api/data", false); // 同步,會阻塞
xhr.send();
console.log(xhr.responseText); // 此時已有回應WARNING
同步請求會阻塞主執行緒,導致頁面無回應。現代瀏覽器在主執行緒中禁止同步 XHR 請求。
十、 XHR 的限制
| 限制 | 說明 |
|---|---|
| 回調地獄 | 複雜邏輯導致巢狀回調 |
| 無 Promise | 需要自己封裝 |
| API 老舊 | 不如 Fetch 直覺 |
| 無串流支援 | 無法處理原生串流 |
這就是 Fetch API 誕生的原因!
總結
| 概念 | 說明 |
|---|---|
| XHR | 瀏覽器原生 HTTP 請求 API |
| readyState | 5 種狀態追蹤請求生命週期 |
| 事件 | load/error/progress 等 |
| responseType | text/json/blob/arraybuffer |
| withCredentials | 跨域攜帶 Cookie |
> **現代開發建議**:
- 新專案使用 Fetch API
- 需要進度監控時考慮 XHR
- 使用 Axios 等函式庫封裝
進階挑戰
- 使用 XHR 實作一個帶進度條的檔案上傳功能。
- 封裝一個支援自動重試的 XHR Promise 包裝器。
- 比較 XHR 和 Fetch 在處理串流資料時的差異。