Javascript XMLHttpRequest

2023-02-17 10:57 更新

?XMLHttpRequest? 是一個(gè)內(nèi)建的瀏覽器對(duì)象,它允許使用 JavaScript 發(fā)送 HTTP 請(qǐng)求。

雖然它的名字里面有 “XML” 一詞,但它可以操作任何數(shù)據(jù),而不僅僅是 XML 格式。我們可以用它來(lái)上傳/下載文件,跟蹤進(jìn)度等。

現(xiàn)如今,我們有一個(gè)更為現(xiàn)代的方法叫做 fetch,它的出現(xiàn)使得 XMLHttpRequest 在某種程度上被棄用。

在現(xiàn)代 Web 開(kāi)發(fā)中,出于以下三種原因,我們還在使用 XMLHttpRequest

  1. 歷史原因:我們需要支持現(xiàn)有的使用了 ?XMLHttpRequest? 的腳本。
  2. 我們需要兼容舊瀏覽器,并且不想用 polyfill(例如為了使腳本更?。?/li>
  3. 我們需要做一些 ?fetch? 目前無(wú)法做到的事情,例如跟蹤上傳進(jìn)度。

這些話聽(tīng)起來(lái)熟悉嗎?如果是,那么請(qǐng)繼續(xù)閱讀下面的 XMLHttpRequest 相關(guān)內(nèi)容吧。如果還不是很熟悉的話,那么請(qǐng)先閱讀 Fetch 一章的內(nèi)容。

XMLHttpRequest 基礎(chǔ)

XMLHttpRequest 有兩種執(zhí)行模式:同步(synchronous)和異步(asynchronous)。

我們首先來(lái)看看最常用的異步模式:

要發(fā)送請(qǐng)求,需要 3 個(gè)步驟:

  1. 創(chuàng)建 XMLHttpRequest
  2. let xhr = new XMLHttpRequest();
    

    此構(gòu)造器沒(méi)有參數(shù)。

  3. 初始化它,通常就在 new XMLHttpRequest 之后:
  4. xhr.open(method, URL, [async, user, password])
    

    此方法指定請(qǐng)求的主要參數(shù):

    • ?method? —— HTTP 方法。通常是 ?"GET"? 或 ?"POST"?。
    • ?URL? —— 要請(qǐng)求的 URL,通常是一個(gè)字符串,也可以是 URL 對(duì)象。
    • ?async? —— 如果顯式地設(shè)置為 ?false?,那么請(qǐng)求將會(huì)以同步的方式處理,我們稍后會(huì)講到它。
    • ?user?,?password? —— HTTP 基本身份驗(yàn)證(如果需要的話)的登錄名和密碼。

    請(qǐng)注意,open 調(diào)用與其名稱相反,不會(huì)建立連接。它僅配置請(qǐng)求,而網(wǎng)絡(luò)活動(dòng)僅以 send 調(diào)用開(kāi)啟。

  5. 發(fā)送請(qǐng)求。
  6. xhr.send([body])
    

    這個(gè)方法會(huì)建立連接,并將請(qǐng)求發(fā)送到服務(wù)器??蛇x參數(shù) body 包含了 request body。

    一些請(qǐng)求方法,像 GET 沒(méi)有 request body。還有一些請(qǐng)求方法,像 POST 使用 body 將數(shù)據(jù)發(fā)送到服務(wù)器。我們稍后會(huì)看到相應(yīng)示例。

  7. 監(jiān)聽(tīng) xhr 事件以獲取響應(yīng)。
  8. 這三個(gè)事件是最常用的:

    • ?load? —— 當(dāng)請(qǐng)求完成(即使 HTTP 狀態(tài)為 400 或 500 等),并且響應(yīng)已完全下載。
    • ?error? —— 當(dāng)無(wú)法發(fā)出請(qǐng)求,例如網(wǎng)絡(luò)中斷或者無(wú)效的 URL。
    • ?progress? —— 在下載響應(yīng)期間定期觸發(fā),報(bào)告已經(jīng)下載了多少。
    xhr.onload = function() {
      alert(`Loaded: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // 僅在根本無(wú)法發(fā)出請(qǐng)求時(shí)觸發(fā)
      alert(`Network Error`);
    };
    
    xhr.onprogress = function(event) { // 定期觸發(fā)
      // event.loaded —— 已經(jīng)下載了多少字節(jié)
      // event.lengthComputable = true,當(dāng)服務(wù)器發(fā)送了 Content-Length header 時(shí)
      // event.total —— 總字節(jié)數(shù)(如果 lengthComputable 為 true)
      alert(`Received ${event.loaded} of ${event.total}`);
    };

下面是一個(gè)完整的示例。它從服務(wù)器加載 /article/xmlhttprequest/example/load,并打印加載進(jìn)度:

// 1. 創(chuàng)建一個(gè) new XMLHttpRequest 對(duì)象
let xhr = new XMLHttpRequest();

// 2. 配置它:從 URL /article/.../load GET-request
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. 通過(guò)網(wǎng)絡(luò)發(fā)送請(qǐng)求
xhr.send();

// 4. 當(dāng)接收到響應(yīng)后,將調(diào)用此函數(shù)
xhr.onload = function() {
  if (xhr.status != 200) { // 分析響應(yīng)的 HTTP 狀態(tài)
    alert(`Error ${xhr.status}: ${xhr.statusText}`); // 例如 404: Not Found
  } else { // 顯示結(jié)果
    alert(`Done, got ${xhr.response.length} bytes`); // response 是服務(wù)器響應(yīng)
  }
};

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

};

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

一旦服務(wù)器有了響應(yīng),我們可以在以下 xhr 屬性中接收結(jié)果:

?status ?

HTTP 狀態(tài)碼(一個(gè)數(shù)字):?200?,?404?,?403? 等,如果出現(xiàn)非 HTTP 錯(cuò)誤,則為 ?0?。

?statusText ?

HTTP 狀態(tài)消息(一個(gè)字符串):狀態(tài)碼為 ?200? 對(duì)應(yīng)于 ?OK?,?404? 對(duì)應(yīng)于 ?Not Found?,?403? 對(duì)應(yīng)于 ?Forbidden?。

?response?(舊腳本可能用的是 ?responseText?)

服務(wù)器 response body。

我們還可以使用相應(yīng)的屬性指定超時(shí)(timeout):

xhr.timeout = 10000; // timeout 單位是 ms,此處即 10 秒

如果在給定時(shí)間內(nèi)請(qǐng)求沒(méi)有成功執(zhí)行,請(qǐng)求就會(huì)被取消,并且觸發(fā) timeout 事件。

URL 搜索參數(shù)(URL search parameters)

為了向 URL 添加像 ?name=value 這樣的參數(shù),并確保正確的編碼,我們可以使用 URL 對(duì)象:

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

// 參數(shù) 'q' 被編碼
xhr.open('GET', url); // https://google.com/search?q=test+me%21

響應(yīng)類型

我們可以使用 xhr.responseType 屬性來(lái)設(shè)置響應(yīng)格式:

  • ?""?(默認(rèn))—— 響應(yīng)格式為字符串,
  • ?"text"? —— 響應(yīng)格式為字符串,
  • ?"arraybuffer"? —— 響應(yīng)格式為 ?ArrayBuffer?(對(duì)于二進(jìn)制數(shù)據(jù),請(qǐng)參見(jiàn) ArrayBuffer,二進(jìn)制數(shù)組),
  • ?"blob"? —— 響應(yīng)格式為 ?Blob?(對(duì)于二進(jìn)制數(shù)據(jù),請(qǐng)參見(jiàn) Blob),
  • ?"document"? —— 響應(yīng)格式為 XML document(可以使用 XPath 和其他 XML 方法)或 HTML document(基于接收數(shù)據(jù)的 MIME 類型)
  • ?"json"? —— 響應(yīng)格式為 JSON(自動(dòng)解析)。

例如,我們以 JSON 格式獲取響應(yīng):

let xhr = new XMLHttpRequest();

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

xhr.responseType = 'json';

xhr.send();

// 響應(yīng)為 {"message": "Hello, world!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Hello, world!
};
請(qǐng)注意:

在舊的腳本中,你可能會(huì)看到 xhr.responseText,甚至?xí)吹?nbsp;xhr.responseXML 屬性。

它們是由于歷史原因而存在的,以獲取字符串或 XML 文檔。如今,我們應(yīng)該在 xhr.responseType 中設(shè)置格式,然后就能獲取如上所示的 xhr.response 了。

readyState

XMLHttpRequest 的狀態(tài)(state)會(huì)隨著它的處理進(jìn)度變化而變化??梢酝ㄟ^(guò) xhr.readyState 來(lái)了解當(dāng)前狀態(tài)。

規(guī)范 中提到的所有狀態(tài)如下:

UNSENT = 0; // 初始狀態(tài)
OPENED = 1; // open 被調(diào)用
HEADERS_RECEIVED = 2; // 接收到 response header
LOADING = 3; // 響應(yīng)正在被加載(接收到一個(gè)數(shù)據(jù)包)
DONE = 4; // 請(qǐng)求完成

XMLHttpRequest 對(duì)象以 0 → 1 → 2 → 3 → … → 3 → 4 的順序在它們之間轉(zhuǎn)變。每當(dāng)通過(guò)網(wǎng)絡(luò)接收到一個(gè)數(shù)據(jù)包,就會(huì)重復(fù)一次狀態(tài) 3。

我們可以使用 readystatechange 事件來(lái)跟蹤它們:

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // 加載中
  }
  if (xhr.readyState == 4) {
    // 請(qǐng)求完成
  }
};

你可能在非常老的代碼中找到 readystatechange 這樣的事件監(jiān)聽(tīng)器,它的存在是有歷史原因的,因?yàn)樵?jīng)有很長(zhǎng)一段時(shí)間都沒(méi)有 load 以及其他事件。如今,它已被 load/error/progress 事件處理程序所替代。

中止請(qǐng)求(Aborting)

我們可以隨時(shí)終止請(qǐng)求。調(diào)用 ?xhr.abort()? 即可:

xhr.abort(); // 終止請(qǐng)求

它會(huì)觸發(fā) abort 事件,且 xhr.status 變?yōu)?nbsp;0。

同步請(qǐng)求

如果在 open 方法中將第三個(gè)參數(shù) async 設(shè)置為 false,那么請(qǐng)求就會(huì)以同步的方式進(jìn)行。

換句話說(shuō),JavaScript 執(zhí)行在 send() 處暫停,并在收到響應(yīng)后恢復(fù)執(zhí)行。這有點(diǎn)兒像 alert 或 prompt 命令。

下面是重寫(xiě)的示例,open 的第三個(gè)參數(shù)為 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) { // 代替 onerror
  alert("Request failed");
}

這看起來(lái)好像不錯(cuò),但是很少使用同步調(diào)用,因?yàn)樗鼈儠?huì)阻塞頁(yè)面內(nèi)的 JavaScript,直到加載完成。在某些瀏覽器中,滾動(dòng)可能無(wú)法正常進(jìn)行。如果一個(gè)同步調(diào)用執(zhí)行時(shí)間過(guò)長(zhǎng),瀏覽器可能會(huì)建議關(guān)閉“掛起(hanging)”的網(wǎng)頁(yè)。

XMLHttpRequest 的很多高級(jí)功能在同步請(qǐng)求中都不可用,例如向其他域發(fā)起請(qǐng)求或者設(shè)置超時(shí)。并且,正如你所看到的,沒(méi)有進(jìn)度指示。

基于這些原因,同步請(qǐng)求使用的非常少,幾乎從不使用。在這我們就不再討論它了。

HTTP-header

XMLHttpRequest 允許發(fā)送自定義 header,并且可以從響應(yīng)中讀取 header。

HTTP-header 有三種方法:

?setRequestHeader(name, value)?

使用給定的 name 和 value 設(shè)置 request header。

例如:

xhr.setRequestHeader('Content-Type', 'application/json');

Header 的限制

一些 header 是由瀏覽器專門(mén)管理的,例如 Referer 和 Host。 完整列表請(qǐng)見(jiàn) 規(guī)范。

為了用戶安全和請(qǐng)求的正確性,XMLHttpRequest 不允許更改它們。

不能移除 header

XMLHttpRequest 的另一個(gè)特點(diǎn)是不能撤銷 setRequestHeader。

一旦設(shè)置了 header,就無(wú)法撤銷了。其他調(diào)用會(huì)向 header 中添加信息,但不會(huì)覆蓋它。

例如:

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

// header 將是:
// X-Auth: 123, 456

?getResponseHeader(name)?

獲取具有給定 name 的 header(Set-Cookie 和 Set-Cookie2 除外)。

例如:

xhr.getResponseHeader('Content-Type')

?getAllResponseHeaders()?

返回除 Set-Cookie 和 Set-Cookie2 外的所有 response header。

header 以單行形式返回,例如:

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

header 之間的換行符始終為 "\r\n"(不依賴于操作系統(tǒng)),所以我們可以很容易地將其拆分為單獨(dú)的 header。name 和 value 之間總是以冒號(hào)后跟一個(gè)空格 ": " 分隔。這是標(biāo)準(zhǔn)格式。

因此,如果我們想要獲取具有 name/value 對(duì)的對(duì)象,則需要用一點(diǎn) JavaScript 代碼來(lái)處理它們。

像這樣(假設(shè)如果兩個(gè) header 具有相同的名稱,那么后者就會(huì)覆蓋前者):

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,F(xiàn)ormData

要建立一個(gè) POST 請(qǐng)求,我們可以使用內(nèi)建的 FormData 對(duì)象。

語(yǔ)法為:

let formData = new FormData([form]); // 創(chuàng)建一個(gè)對(duì)象,可以選擇從 <form> 中獲取數(shù)據(jù)
formData.append(name, value); // 附加一個(gè)字段

我們創(chuàng)建它,可以選擇從一個(gè)表單中獲取數(shù)據(jù),如果需要,還可以 append 更多字段,然后:

  1. ?xhr.open('POST', ...)? —— 使用 ?POST? 方法。
  2. ?xhr.send(formData)? 將表單發(fā)送到服務(wù)器。

例如:

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

<script>
  // 從表單預(yù)填充 FormData
  let formData = new FormData(document.forms.person);

  // 附加一個(gè)字段
  formData.append("middle", "Lee");

  // 將其發(fā)送出去
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

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

以 multipart/form-data 編碼發(fā)送表單。

或者,如果我們更喜歡 JSON,那么可以使用 JSON.stringify 并以字符串形式發(fā)送。

只是,不要忘記設(shè)置 header Content-Type: application/json,只要有了它,很多服務(wù)端框架都能自動(dòng)解碼 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) 方法就像一個(gè)非常雜食性的動(dòng)物。它幾乎可以發(fā)送任何 body,包括 Blob 和 BufferSource 對(duì)象。

上傳進(jìn)度

?progress? 事件僅在下載階段觸發(fā)。

也就是說(shuō):如果我們 POST 一些內(nèi)容,XMLHttpRequest 首先上傳我們的數(shù)據(jù)(request body),然后下載響應(yīng)。

如果我們要上傳的東西很大,那么我們肯定會(huì)對(duì)跟蹤上傳進(jìn)度感興趣。但是 xhr.onprogress 在這里并不起作用。

這里有另一個(gè)對(duì)象,它沒(méi)有方法,它專門(mén)用于跟蹤上傳事件:xhr.upload。

它會(huì)生成事件,類似于 xhr,但是 xhr.upload 僅在上傳時(shí)觸發(fā)它們:

  • ?loadstart? —— 上傳開(kāi)始。
  • ?progress? —— 上傳期間定期觸發(fā)。
  • ?abort? —— 上傳中止。
  • ?error? —— 非 HTTP 錯(cuò)誤。
  • ?load? —— 上傳成功完成。
  • ?timeout? —— 上傳超時(shí)(如果設(shè)置了 ?timeout? 屬性)。
  • ?loadend? —— 上傳完成,無(wú)論成功還是 error。

handler 示例:

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}`);
};

這是一個(gè)真實(shí)示例:帶有進(jìn)度指示的文件上傳:

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

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

  // 跟蹤上傳進(jìn)度
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  // 跟蹤完成:無(wú)論成功與否
  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>

跨源請(qǐng)求

XMLHttpRequest 可以使用和 fetch 相同的 CORS 策略進(jìn)行跨源請(qǐng)求。

就像 fetch 一樣,默認(rèn)情況下不會(huì)將 cookie 和 HTTP 授權(quán)發(fā)送到其他域。要啟用它們,可以將 xhr.withCredentials 設(shè)置為 true

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

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

有關(guān)跨源 header 的詳細(xì)信息,請(qǐng)見(jiàn) Fetch:跨源請(qǐng)求 一章。

總結(jié)

使用 XMLHttpRequest 的 GET 請(qǐng)求的典型代碼:

let xhr = new XMLHttpRequest();

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

xhr.send();

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

  // 獲取來(lái)自 xhr.response 的響應(yīng)
};

xhr.onprogress = function(event) {
  // 報(bào)告進(jìn)度
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

xhr.onerror = function() {
  // 處理非 HTTP error(例如網(wǎng)絡(luò)中斷)
};

實(shí)際上還有很多事件,在 現(xiàn)代規(guī)范 中有詳細(xì)列表(按生命周期排序):

  • ?loadstart? —— 請(qǐng)求開(kāi)始。
  • ?progress? —— 一個(gè)響應(yīng)數(shù)據(jù)包到達(dá),此時(shí)整個(gè) response body 都在 ?response? 中。
  • ?abort? —— 調(diào)用 ?xhr.abort()? 取消了請(qǐng)求。
  • error —— 發(fā)生連接錯(cuò)誤,例如,域錯(cuò)誤。不會(huì)發(fā)生諸如 404 這類的 HTTP 錯(cuò)誤。
  • load —— 請(qǐng)求成功完成。
  • timeout —— 由于請(qǐng)求超時(shí)而取消了該請(qǐng)求(僅發(fā)生在設(shè)置了 timeout 的情況下)。
  • loadend —— 在 ?load?,?error?,?timeout? 或 ?abort? 之后觸發(fā)。

error,aborttimeout 和 load 事件是互斥的。其中只有一種可能發(fā)生。

最常用的事件是加載完成(load),加載失敗(error),或者我們可以使用單個(gè) loadend 處理程序并檢查請(qǐng)求對(duì)象 xhr 的屬性,以查看發(fā)生了什么。

我們還了解了另一個(gè)事件:readystatechange。由于歷史原因,它早在規(guī)范制定之前就出現(xiàn)了。如今我們已經(jīng)無(wú)需使用它了,我們可以用新的事件代替它,但通常可以在舊的代碼中找到它。

如果我們需要專門(mén)跟蹤上傳,那么我們應(yīng)該在 xhr.upload 對(duì)象上監(jiān)聽(tīng)相同的事件。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)