本文 AI 產出,尚未審核

JavaScript 課程 – Fetch 與網路請求(Networking)

主題:XMLHttpRequest(舊 API)


簡介

在前端開發中,與伺服器交換資料是最常見的需求之一。雖然現在 fetch 已成為官方推薦的方式,但 XMLHttpRequest(簡稱 XHR) 仍然在許多既有專案、舊版瀏覽器或需要更細緻控制的情境下被廣泛使用。了解 XHR 的運作原理不僅能幫助你排除舊程式的問題,也能讓你在需要時靈活選擇最適合的 API。

本篇文章將從 核心概念實作範例常見陷阱與最佳實踐,到 實際應用場景,一步步帶你掌握 XHR,讓你在任何環境下都能自信地處理網路請求。


核心概念

1. XMLHttpRequest 的生命週期

XMLHttpRequest 物件在建立後會經歷以下幾個重要階段:

階段 readyState 值 說明
UNSET (未初始化) 0 物件已建立,但尚未呼叫 open()
OPENED (已開啟) 1 呼叫 open() 後,設定請求方法、URL 等資訊。
HEADERS_RECEIVED (已接收回應標頭) 2 伺服器已回傳回應標頭。
LOADING (正在接收回應內容) 3 回應內容正以文字或二進位方式逐段下載。
DONE (完成) 4 請求已完成,無論成功或失敗,都會觸發此狀態。

透過 onreadystatechange 事件,我們可以在不同階段取得回傳資料或錯誤資訊。


2. 基本使用流程

  1. 建立物件var xhr = new XMLHttpRequest();
  2. 設定請求xhr.open(method, url, async, user, password);
    • methodGETPOSTPUTDELETE 等 HTTP 方法。
    • url:目標資源的完整或相對路徑。
    • async(布林值):是否非同步(建議使用 true)。
  3. 設定請求標頭(可選)xhr.setRequestHeader(name, value);
  4. 註冊事件xhr.onreadystatechange = function() { … }xhr.onload / xhr.onerror
  5. 送出請求xhr.send(body);
    • bodyGET 時通常為 nullPOST 時可傳送 FormDataJSONBlob 等。

3. 同步 vs 非同步

  • 非同步(預設):UI 不會被阻塞,適合大多數情境。
  • 同步async 參數設為 false,瀏覽器會在請求完成前凍結 UI,僅在特殊需求(如測試環境)下才使用,已被大多數瀏覽器警告或棄用。

4. 取得回應資料

  • xhr.responseText:回傳的文字字串(預設)。
  • xhr.response:根據 responseType 取得不同型別(jsonblobarraybuffer 等)。
  • xhr.status:HTTP 狀態碼(200、404、500…)。
  • xhr.getAllResponseHeaders():取得所有回應標頭。

程式碼範例

以下提供 五個 常見且實用的 XHR 範例,每段程式碼皆附有說明註解,方便你快速上手。

範例 1️⃣ 簡易 GET 請求(取得 JSON)

// 取得遠端 JSON 資料
function fetchJSON(url) {
  const xhr = new XMLHttpRequest();

  // 設定為 GET,且非同步
  xhr.open('GET', url, true);

  // 設定回應類型為 JSON,瀏覽器會自動解析
  xhr.responseType = 'json';

  // 監聽請求完成事件
  xhr.onload = function () {
    if (xhr.status === 200) {
      console.log('取得資料:', xhr.response);
    } else {
      console.error('伺服器回傳錯誤,狀態碼:', xhr.status);
    }
  };

  // 錯誤處理
  xhr.onerror = function () {
    console.error('網路錯誤,請檢查連線');
  };

  // 送出請求
  xhr.send();
}

// 使用範例
fetchJSON('https://jsonplaceholder.typicode.com/posts/1');

重點:設定 responseType = 'json' 後,xhr.response 直接是已解析的物件,省去 JSON.parse()


範例 2️⃣ POST JSON 資料並取得回應

function postJSON(url, data) {
  const xhr = new XMLHttpRequest();

  xhr.open('POST', url, true);
  // 告訴伺服器請求主體是 JSON
  xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');

  // 期待回傳 JSON
  xhr.responseType = 'json';

  xhr.onload = function () {
    if (xhr.status === 201) { // 建立成功的慣用狀態碼
      console.log('新增成功:', xhr.response);
    } else {
      console.warn('伺服器回傳非預期狀態碼:', xhr.status);
    }
  };

  xhr.onerror = function () {
    console.error('請求失敗,可能是網路問題');
  };

  // 將 JavaScript 物件序列化為 JSON 字串
  xhr.send(JSON.stringify(data));
}

// 範例呼叫
postJSON('https://jsonplaceholder.typicode.com/posts', {
  title: '測試文章',
  body: '這是一段測試內容',
  userId: 1
});

技巧Content-Type: application/json 必須與 JSON.stringify() 同時使用,否則伺服器可能無法正確解析。


範例 3️⃣ 上傳檔案(FormData)

function uploadFile(url, fileInputId) {
  const fileInput = document.getElementById(fileInputId);
  if (!fileInput.files.length) {
    alert('請先選擇檔案');
    return;
  }

  const formData = new FormData();
  formData.append('file', fileInput.files[0]); // key 為 'file'

  const xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);

  // 監聽上傳進度
  xhr.upload.onprogress = function (e) {
    if (e.lengthComputable) {
      const percent = Math.round((e.loaded / e.total) * 100);
      console.log(`上傳進度:${percent}%`);
    }
  };

  xhr.onload = function () {
    if (xhr.status === 200) {
      console.log('檔案上傳成功', xhr.responseText);
    } else {
      console.error('上傳失敗,狀態碼:', xhr.status);
    }
  };

  xhr.onerror = function () {
    console.error('上傳過程發生錯誤');
  };

  xhr.send(formData);
}

// HTML 範例
// <input type="file" id="myFile">
// <button onclick="uploadFile('/upload', 'myFile')">上傳</button>

說明:使用 FormData不需要 手動設定 Content-Type,瀏覽器會自動加上 multipart/form-data 以及正確的邊界字串。


範例 4️⃣ 讀取二進位檔案(Blob)並顯示圖片

function loadImage(url, imgElementId) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);
  xhr.responseType = 'blob'; // 取得 Blob 物件

  xhr.onload = function () {
    if (xhr.status === 200) {
      const blob = xhr.response;
      const imgURL = URL.createObjectURL(blob);
      document.getElementById(imgElementId).src = imgURL;
    } else {
      console.error('圖片載入失敗,狀態碼:', xhr.status);
    }
  };

  xhr.onerror = function () {
    console.error('網路錯誤,圖片無法取得');
  };

  xhr.send();
}

// 使用範例
// <img id="preview" alt="預覽圖">
loadImage('https://picsum.photos/400/300', 'preview');

小技巧URL.createObjectURL() 可將 Blob 轉成臨時 URL,適合即時預覽或下載。


範例 5️⃣ 使用 onreadystatechange 完整流程(舊寫法)

function legacyGet(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url, true);

  // 這裡使用舊式的 readyState 檢查
  xhr.onreadystatechange = function () {
    // readyState 4 表示請求已完成
    if (xhr.readyState === 4) {
      // 只在 HTTP 200~299 之間視為成功
      if (xhr.status >= 200 && xhr.status < 300) {
        callback(null, xhr.responseText);
      } else {
        callback(new Error('HTTP 錯誤:' + xhr.status));
      }
    }
  };

  xhr.onerror = function () {
    callback(new Error('網路錯誤'));
  };

  xhr.send();
}

// 呼叫方式
legacyGet('https://jsonplaceholder.typicode.com/posts/2', function (err, data) {
  if (err) {
    console.error(err);
    return;
  }
  console.log('取得資料:', JSON.parse(data));
});

提醒onreadystatechange 雖然仍可使用,但相較於 onload/onerror 可讀性較差,建議新專案以 load / error 為主。


常見陷阱與最佳實踐

陷阱 說明 最佳實踐
同步 XHR async = false 會凍結 UI,使用者體驗極差。 永遠使用非同步模式;若必須同步,僅限於 Worker 或測試環境。
忘記設定 Content-Type 伺服器無法正確解析請求主體。 針對 POST/PUT 明確設定 Content-Typeapplication/jsonapplication/x-www-form-urlencoded 等)。
跨域 (CORS) 錯誤 瀏覽器阻擋未授權的跨域請求,會得到 status = 0 確認伺服器回傳正確的 Access-Control-Allow-Origin,或使用同源代理。
未處理錯誤 只檢查 status === 200,忽略 4xx/5xx 或網路斷線。 同時使用 onerrorontimeout,並根據 status 給予使用者回饋。
記憶體洩漏 未釋放 URL.createObjectURL 或未移除事件監聽。 使用 URL.revokeObjectURL() 釋放 Blob URL,並在不需要時 xhr = null
未設定 responseType 取得二進位資料時仍以文字呈現,導致資料損毀。 依需求設定 `responseType = 'json'

額外的最佳實踐

  1. 設定 timeoutxhr.timeout = 15000; // 15 秒,避免請求無限等待。
  2. 使用 try...catch 包裹 JSON 解析:即使 responseType='json',仍可能因伺服器回傳非 JSON 而拋錯。
  3. 封裝成 Promise:雖然 XHR 本身不是 Promise,但可以自行包裝,讓呼叫方式更符合現代寫法。
function xhrPromise(method, url, data = null) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.responseType = 'json';
    xhr.onload = () => (xhr.status >= 200 && xhr.status < 300) ? resolve(xhr.response) : reject(xhr);
    xhr.onerror = () => reject(xhr);
    if (data) {
      xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
      xhr.send(JSON.stringify(data));
    } else {
      xhr.send();
    }
  });
}

實際應用場景

場景 為何仍會使用 XHR
舊版瀏覽器支援 IE9~IE11 尚未原生支援 fetch,企業內部系統常仍需兼容。
需要 upload 進度監控 XMLHttpRequest.upload 提供 onprogress 事件,fetch 目前尚未支援檔案上傳進度。
跨域需求配合舊版伺服器 某些舊 API 只接受特定 Header,使用 XHR 可更細緻控制。
嵌入式系統或 WebView 某些嵌入式瀏覽器僅實作 XHR,缺少 fetch
學習與除錯 理解底層的 readyStatestatus 等概念,有助於掌握其他 HTTP 客戶端的行為。

總結

  • XMLHttpRequest 是瀏覽器最早提供的 AJAX 機制,雖然已被 fetch 替代,但在 相容性、上傳進度監控 以及 舊系統維護 等情境仍具不可取代的價值。
  • 掌握 生命週期(readyState)非同步使用正確設定 Header、以及 錯誤處理,即可寫出穩定且易維護的網路程式碼。
  • 建議在新專案中以 fetch 為主,但 將 XHR 包裝成 Promise,可在需要時無縫切換,同時保留舊系統的相容性。

透過本文的概念與範例,你已具備在任何前端環境下使用 XMLHttpRequest 進行資料交換的能力。未來若遇到舊版瀏覽器或需要細緻控制的情況,別忘了這個「老朋友」仍然可以為你解決問題。祝開發順利!