Javascript XMLHttpRequest

2023-02-17 10:57 更新

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

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

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

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

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

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

XMLHttpRequest 基礎(chǔ)

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

我們首先來看看最常用的異步模式:

要發(fā)送請求,需要 3 個步驟:

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

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

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

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

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

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

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

    這個方法會建立連接,并將請求發(fā)送到服務(wù)器。可選參數(shù) body 包含了 request body。

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

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

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

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

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

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

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

// 4. 當接收到響應(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`); // 沒有 Content-Length
  }

};

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

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

?status ?

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

?statusText ?

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

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

服務(wù)器 response body。

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

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

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

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

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

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 屬性來設(shè)置響應(yīng)格式:

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

例如,我們以 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!
};
請注意:

在舊的腳本中,你可能會看到 xhr.responseText,甚至會看到 xhr.responseXML 屬性。

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

readyState

XMLHttpRequest 的狀態(tài)(state)會隨著它的處理進度變化而變化??梢酝ㄟ^ xhr.readyState 來了解當前狀態(tài)。

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

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

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

我們可以使用 readystatechange 事件來跟蹤它們:

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

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

中止請求(Aborting)

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

xhr.abort(); // 終止請求

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

同步請求

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

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

下面是重寫的示例,open 的第三個參數(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");
}

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

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

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

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 是由瀏覽器專門管理的,例如 Referer 和 Host。 完整列表請見 規(guī)范。

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

不能移除 header

XMLHttpRequest 的另一個特點是不能撤銷 setRequestHeader。

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

例如:

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)),所以我們可以很容易地將其拆分為單獨的 header。name 和 value 之間總是以冒號后跟一個空格 ": " 分隔。這是標準格式。

因此,如果我們想要獲取具有 name/value 對的對象,則需要用一點 JavaScript 代碼來處理它們。

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

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

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

語法為:

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

我們創(chuàng)建它,可以選擇從一個表單中獲取數(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);

  // 附加一個字段
  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ù)端框架都能自動解碼 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) 方法就像一個非常雜食性的動物。它幾乎可以發(fā)送任何 body,包括 Blob 和 BufferSource 對象。

上傳進度

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

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

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

這里有另一個對象,它沒有方法,它專門用于跟蹤上傳事件:xhr.upload。

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

  • ?loadstart? —— 上傳開始。
  • ?progress? —— 上傳期間定期觸發(fā)。
  • ?abort? —— 上傳中止。
  • ?error? —— 非 HTTP 錯誤。
  • ?load? —— 上傳成功完成。
  • ?timeout? —— 上傳超時(如果設(shè)置了 ?timeout? 屬性)。
  • ?loadend? —— 上傳完成,無論成功還是 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}`);
};

這是一個真實示例:帶有進度指示的文件上傳:

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

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

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

  // 跟蹤完成:無論成功與否
  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 授權(quán)發(fā)送到其他域。要啟用它們,可以將 xhr.withCredentials 設(shè)置為 true

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

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

有關(guān)跨源 header 的詳細信息,請見 Fetch:跨源請求 一章。

總結(jié)

使用 XMLHttpRequest 的 GET 請求的典型代碼:

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

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

xhr.onprogress = function(event) {
  // 報告進度
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

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

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

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

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號