跳至主要內容
Skip to content

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常量說明
0UNSENT尚未呼叫 open()
1OPENED已呼叫 open()
2HEADERS_RECEIVED收到回應標頭
3LOADING正在接收 Body
4DONE完成

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 類型
'' (空)預設,視為 textstring
'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
readyState5 種狀態追蹤請求生命週期
事件load/error/progress 等
responseTypetext/json/blob/arraybuffer
withCredentials跨域攜帶 Cookie

> **現代開發建議**:

  • 新專案使用 Fetch API
  • 需要進度監控時考慮 XHR
  • 使用 Axios 等函式庫封裝

進階挑戰

  1. 使用 XHR 實作一個帶進度條的檔案上傳功能。
  2. 封裝一個支援自動重試的 XHR Promise 包裝器。
  3. 比較 XHR 和 Fetch 在處理串流資料時的差異。

延伸閱讀與資源