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. 基本使用流程
- 建立物件:
var xhr = new XMLHttpRequest(); - 設定請求:
xhr.open(method, url, async, user, password);method:GET、POST、PUT、DELETE等 HTTP 方法。url:目標資源的完整或相對路徑。async(布林值):是否非同步(建議使用true)。
- 設定請求標頭(可選):
xhr.setRequestHeader(name, value); - 註冊事件:
xhr.onreadystatechange = function() { … }或xhr.onload / xhr.onerror。 - 送出請求:
xhr.send(body);body為GET時通常為null,POST時可傳送FormData、JSON、Blob等。
3. 同步 vs 非同步
- 非同步(預設):UI 不會被阻塞,適合大多數情境。
- 同步:
async參數設為false,瀏覽器會在請求完成前凍結 UI,僅在特殊需求(如測試環境)下才使用,已被大多數瀏覽器警告或棄用。
4. 取得回應資料
xhr.responseText:回傳的文字字串(預設)。xhr.response:根據responseType取得不同型別(json、blob、arraybuffer等)。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-Type(application/json、application/x-www-form-urlencoded 等)。 |
| 跨域 (CORS) 錯誤 | 瀏覽器阻擋未授權的跨域請求,會得到 status = 0。 |
確認伺服器回傳正確的 Access-Control-Allow-Origin,或使用同源代理。 |
| 未處理錯誤 | 只檢查 status === 200,忽略 4xx/5xx 或網路斷線。 |
同時使用 onerror、ontimeout,並根據 status 給予使用者回饋。 |
| 記憶體洩漏 | 未釋放 URL.createObjectURL 或未移除事件監聽。 |
使用 URL.revokeObjectURL() 釋放 Blob URL,並在不需要時 xhr = null。 |
未設定 responseType |
取得二進位資料時仍以文字呈現,導致資料損毀。 | 依需求設定 `responseType = 'json' |
額外的最佳實踐:
- 設定 timeout:
xhr.timeout = 15000; // 15 秒,避免請求無限等待。 - 使用
try...catch包裹 JSON 解析:即使responseType='json',仍可能因伺服器回傳非 JSON 而拋錯。 - 封裝成 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。 |
| 學習與除錯 | 理解底層的 readyState、status 等概念,有助於掌握其他 HTTP 客戶端的行為。 |
總結
- XMLHttpRequest 是瀏覽器最早提供的 AJAX 機制,雖然已被
fetch替代,但在 相容性、上傳進度監控 以及 舊系統維護 等情境仍具不可取代的價值。 - 掌握 生命週期(readyState)、非同步使用、正確設定 Header、以及 錯誤處理,即可寫出穩定且易維護的網路程式碼。
- 建議在新專案中以
fetch為主,但 將 XHR 包裝成 Promise,可在需要時無縫切換,同時保留舊系統的相容性。
透過本文的概念與範例,你已具備在任何前端環境下使用 XMLHttpRequest 進行資料交換的能力。未來若遇到舊版瀏覽器或需要細緻控制的情況,別忘了這個「老朋友」仍然可以為你解決問題。祝開發順利!