2022 年 5 月 14 日

XMLHttpRequest

XMLHttpRequest 是內建於瀏覽器的物件,允許在 JavaScript 中發出 HTTP 請求。

儘管名稱中有「XML」,但它可以處理任何資料,不只 XML 格式。我們可以上傳/下載檔案、追蹤進度等等。

目前有另一個較新的方法 fetch,在某種程度上取代了 XMLHttpRequest

在現代網頁開發中,XMLHttpRequest 因以下三個原因而被使用

  1. 歷史原因:我們需要支援現有的 XMLHttpRequest 腳本。
  2. 我們需要支援舊瀏覽器,而且不想要多重填充(例如,保持腳本精簡)。
  3. 我們需要 fetch 目前無法執行的功能,例如追蹤上傳進度。

聽起來很熟悉嗎?如果是,那麼沒問題,請繼續使用 XMLHttpRequest。否則,請前往 Fetch

基礎知識

XMLHttpRequest 有兩種操作模式:同步和非同步。

讓我們先看看非同步,因為它在大部分情況下都被使用。

要執行請求,我們需要 3 個步驟

  1. 建立 XMLHttpRequest

    let xhr = new XMLHttpRequest();

    建構函式沒有參數。

  2. 初始化,通常在 new XMLHttpRequest 之後

    xhr.open(method, URL, [async, user, password])

    此方法指定請求的主要參數

    • method – HTTP 方法。通常為 "GET""POST"
    • URL – 要請求的 URL,字串,可以是 URL 物件。
    • async – 如果明確設定為 false,則請求為同步,我們稍後會說明。
    • userpassword – 基本 HTTP 驗證的登入和密碼(如果需要)。

    請注意,open 呼叫與其名稱相反,並未開啟連線。它只會設定請求,但網路活動僅從 send 的呼叫開始。

  3. 發送出去。

    xhr.send([body])

    此方法開啟連線並將請求傳送至伺服器。選用的 body 參數包含請求主體。

    某些請求方法(例如 GET)沒有主體。而某些方法(例如 POST)使用 body 將資料傳送至伺服器。我們稍後會看到範例。

  4. 聆聽 xhr 事件以取得回應。

    這三個事件是最廣泛使用的

    • load – 當請求完成(即使 HTTP 狀態為 400 或 500)且回應已完全下載。
    • error – 當無法進行請求,例如網路中斷或 URL 無效。
    • progress – 在回應下載期間定期觸發,報告已下載的數量。
    xhr.onload = function() {
      alert(`Loaded: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // only triggers if the request couldn't be made at all
      alert(`Network Error`);
    };
    
    xhr.onprogress = function(event) { // triggers periodically
      // event.loaded - how many bytes downloaded
      // event.lengthComputable = true if the server sent Content-Length header
      // event.total - total number of bytes (if lengthComputable)
      alert(`Received ${event.loaded} of ${event.total}`);
    };

以下是一個完整的範例。以下程式碼從伺服器載入 /article/xmlhttprequest/example/load 的 URL 並列印進度

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
  } else { // show the result
    alert(`Done, got ${xhr.response.length} bytes`); // response is the server response
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Received ${event.loaded} of ${event.total} bytes`);
  } else {
    alert(`Received ${event.loaded} bytes`); // no Content-Length
  }

};

xhr.onerror = function() {
  alert("Request failed");
};

伺服器回應後,我們可以在下列 xhr 屬性中接收結果

status
HTTP 狀態碼(數字):200404403 等,在非 HTTP 失敗時可能是 0
statusText
HTTP 狀態訊息(字串):通常 200OK404Not Found403Forbidden 等。
response(舊腳本可能使用 responseText
伺服器回應主體。

我們也可以使用對應屬性指定逾時

xhr.timeout = 10000; // timeout in ms, 10 seconds

如果要求在指定時間內未成功,則會取消要求並觸發 timeout 事件。

URL 搜尋參數

若要將參數新增至 URL,例如 ?name=value,並確保正確編碼,我們可以使用 URL 物件

let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// the parameter 'q' is encoded
xhr.open('GET', url); // https://google.com/search?q=test+me%21

回應類型

我們可以使用 xhr.responseType 屬性來設定回應格式

  • ""(預設)– 取得為字串,
  • "text" – 取得為字串,
  • "arraybuffer" – 取得為 ArrayBuffer(用於二進位資料,請參閱章節 ArrayBuffer、二進位陣列),
  • "blob" – 取得為 Blob(用於二進位資料,請參閱章節 Blob),
  • "document" – 取得為 XML 文件(可以使用 XPath 和其他 XML 方法)或 HTML 文件(根據接收資料的 MIME 類型),
  • "json" – 取得為 JSON(自動剖析)。

例如,讓我們取得回應為 JSON

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

xhr.responseType = 'json';

xhr.send();

// the response is {"message": "Hello, world!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Hello, world!
};
請注意

在舊腳本中,您也可能找到 xhr.responseText 甚至 xhr.responseXML 屬性。

它們存在於歷史原因,用於取得字串或 XML 文件。現在,我們應該在 xhr.responseType 中設定格式,並取得 xhr.response,如上所示。

準備狀態

XMLHttpRequest 會隨著進度而改變狀態。目前的狀態可作為 xhr.readyState 存取。

所有狀態,如 規格 中所述

UNSENT = 0; // initial state
OPENED = 1; // open called
HEADERS_RECEIVED = 2; // response headers received
LOADING = 3; // response is loading (a data packet is received)
DONE = 4; // request complete

XMLHttpRequest 物件會按順序移動它們:0123 → … → 34。每次透過網路接收資料封包時,狀態 3 會重複。

我們可以使用 readystatechange 事件追蹤它們

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // loading
  }
  if (xhr.readyState == 4) {
    // request finished
  }
};

您可以在非常舊的程式碼中找到 readystatechange 監聽器,它存在於歷史原因,因為曾經有一段時間沒有 load 和其他事件。現在,load/error/progress 處理常式已將其棄用。

中止要求

我們可以在任何時候終止要求。呼叫 xhr.abort() 會執行此動作

xhr.abort(); // terminate the request

這會觸發 abort 事件,而且 xhr.status 會變成 0

同步要求

如果在 open 方法中將第三個參數 async 設定為 false,則要求會同步執行。

換句話說,JavaScript 執行會在 send() 暫停,並在收到回應時繼續執行。有點像 alertprompt 指令。

以下是改寫的範例,open 的第三個參數為 false

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Error ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // instead of onerror
  alert("Request failed");
}

這看起來可能不錯,但同步呼叫很少使用,因為它們會在載入完成前封鎖頁面 JavaScript。在某些瀏覽器中,這會導致無法捲動。如果同步呼叫花費太多時間,瀏覽器可能會建議關閉「當機」的網頁。

XMLHttpRequest 的許多進階功能,例如從其他網域要求或指定逾時,都無法用於同步要求。此外,如您所見,沒有進度指示。

由於上述所有原因,同步要求使用得非常少,幾乎從不使用。我們將不再討論它們。

HTTP 標頭

XMLHttpRequest 允許同時傳送自訂標頭和從回應中讀取標頭。

有 3 種 HTTP 標頭方法

setRequestHeader(name, value)

設定具有給定 namevalue 的要求標頭。

例如

xhr.setRequestHeader('Content-Type', 'application/json');
標頭限制

有幾個標頭由瀏覽器獨家管理,例如 RefererHost。完整清單 在規格中

為了使用者的安全和要求的正確性,XMLHttpRequest 不允許變更它們。

無法移除標頭

XMLHttpRequest 的另一個特殊性是無法取消 setRequestHeader

一旦設定標頭,就設定了。其他呼叫會將資訊新增到標頭,而不是覆寫它。

例如

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// the header will be:
// X-Auth: 123, 456
getResponseHeader(name)

取得具有給定 name 的回應標頭(Set-CookieSet-Cookie2 除外)。

例如

xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()

傳回所有回應標頭,Set-CookieSet-Cookie2 除外。

標頭以單行傳回,例如

Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

標頭之間的換行符號永遠是 "\r\n"(與作業系統無關),因此我們可以輕鬆地將其拆分成個別標頭。名稱和值之間的分隔符號永遠是冒號後接空格 ": "。這在規格中是固定的。

因此,如果我們想要取得具有名稱/值對的物件,我們需要加入一些 JS。

像這樣(假設如果兩個標頭具有相同名稱,則後者會覆寫前者)

let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

// headers['Content-Type'] = 'image/png'

POST、FormData

若要發出 POST 要求,我們可以使用內建的 FormData 物件。

語法

let formData = new FormData([form]); // creates an object, optionally fill from <form>
formData.append(name, value); // appends a field

我們建立它,選擇性地從表單填入,視需要附加更多欄位,然後

  1. xhr.open('POST', ...) – 使用 POST 方法。
  2. xhr.send(formData) 將表單提交至伺服器。

例如

<form name="person">
  <input name="name" value="John">
  <input name="surname" value="Smith">
</form>

<script>
  // pre-fill FormData from the form
  let formData = new FormData(document.forms.person);

  // add one more field
  formData.append("middle", "Lee");

  // send it out
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

表單會使用 multipart/form-data 編碼傳送。

或者,如果我們比較喜歡 JSON,則 JSON.stringify 並以字串傳送。

別忘了設定標頭 Content-Type: application/json,許多伺服器端架構會自動使用它來解碼 JSON

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "John",
  surname: "Smith"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

.send(body) 方法相當雜食性。它可以傳送幾乎任何 body,包括 BlobBufferSource 物件。

上傳進度

progress 事件僅會在下載階段觸發。

也就是說:如果我們 POST 了什麼,XMLHttpRequest 會先上傳我們的資料(要求主體),然後下載回應。

如果我們上傳了很大的東西,那麼我們肯定更想追蹤上傳進度。但 xhr.onprogress 在這裡幫不上忙。

還有另一個物件,沒有方法,專門用來追蹤上傳事件:xhr.upload

它會產生事件,類似於 xhr,但 xhr.upload 僅在上傳時觸發它們

  • loadstart – 上傳開始。
  • progress – 在上傳過程中會定期觸發。
  • abort – 上傳中斷。
  • error – 非 HTTP 錯誤。
  • load – 上傳成功完成。
  • timeout – 上傳逾時(如果設定了 timeout 屬性)。
  • loadend – 上傳完成,成功或錯誤。

處理常式範例

xhr.upload.onprogress = function(event) {
  alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};

xhr.upload.onload = function() {
  alert(`Upload finished successfully.`);
};

xhr.upload.onerror = function() {
  alert(`Error during the upload: ${xhr.status}`);
};

以下是一個實際範例:帶進度指示的上傳檔案

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // track upload progress
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  // track completion: both successful or not
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("success");
    } else {
      console.log("error " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

跨來源要求

XMLHttpRequest 可以發出跨來源要求,使用與 fetch 相同的 CORS 政策。

就像 fetch 一樣,它預設不會將 cookie 和 HTTP 授權傳送至其他來源。若要啟用它們,請將 xhr.withCredentials 設為 true

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
...

請參閱章節 Fetch:跨來源要求 以取得有關跨來源標頭的詳細資訊。

摘要

使用 XMLHttpRequest 的 GET 要求的典型程式碼

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP error?
    // handle error
    alert( 'Error: ' + xhr.status);
    return;
  }

  // get the response from xhr.response
};

xhr.onprogress = function(event) {
  // report progress
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

xhr.onerror = function() {
  // handle non-HTTP error (e.g. network down)
};

實際上還有更多事件,現代規格 列出了它們(按生命週期順序)

  • loadstart – 要求已開始。
  • progress – 已經收到回應的資料封包,目前整個回應主體都在 response 中。
  • abort – 要求已由呼叫 xhr.abort() 取消。
  • error – 發生連線錯誤,例如錯誤的網域名稱。不會發生在 HTTP 錯誤(例如 404)上。
  • load – 請求已成功完成。
  • timeout – 請求因逾時而取消(僅在設定逾時時發生)。
  • loadend – 在 loaderrortimeoutabort 之後觸發。

erroraborttimeoutload 事件互斥。只會發生其中一個事件。

最常用的事件是載入完成 (load)、載入失敗 (error),或者我們可以使用單一的 loadend 處理常式並檢查請求物件 xhr 的屬性以查看發生了什麼事。

我們已經看過另一個事件:readystatechange。在規格確定之前,它很早以前就出現了。現在,我們不需要使用它,我們可以用更新的事件取代它,但它經常可以在舊腳本中找到。

如果我們需要特別追蹤上傳,那麼我們應該在 xhr.upload 物件上聆聽相同的事件。

教學地圖

留言

留言前請先閱讀…
  • 如果您有改進建議 - 請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容 - 請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,對於多行 - 將它們包裝在 <pre> 標籤中,對於超過 10 行 - 使用沙盒 (plnkrjsbincodepen…)