Javascript Fetch

2023-02-17 10:57 更新

JavaScript 可以將網(wǎng)絡(luò)請求發(fā)送到服務(wù)器,并在需要時加載新信息。

例如,我們可以使用網(wǎng)絡(luò)請求來:

  • 提交訂單,
  • 加載用戶信息,
  • 從服務(wù)器接收最新的更新,
  • ……等。

……所有這些都沒有重新加載頁面!

對于來自 JavaScript 的網(wǎng)絡(luò)請求,有一個總稱術(shù)語 “AJAX”(Asynchronous JavaScript And XML 的簡稱)。但是,我們不必使用 XML:這個術(shù)語誕生于很久以前,所以這個詞一直在那兒。

有很多方式可以向服務(wù)器發(fā)送網(wǎng)絡(luò)請求,并從服務(wù)器獲取信息。

fetch() 方法是一種現(xiàn)代通用的方法,那么我們就從它開始吧。舊版本的瀏覽器不支持它(可以 polyfill),但是它在現(xiàn)代瀏覽器中的支持情況很好。

基本語法:

let promise = fetch(url, [options])
  • ?url? —— 要訪問的 URL。
  • ?options? —— 可選參數(shù):method,header 等。

沒有 options,這就是一個簡單的 GET 請求,下載 url 的內(nèi)容。

瀏覽器立即啟動請求,并返回一個該調(diào)用代碼應(yīng)該用來獲取結(jié)果的 promise。

獲取響應(yīng)通常需要經(jīng)過兩個階段。

第一階段,當(dāng)服務(wù)器發(fā)送了響應(yīng)頭(response header),fetch 返回的 promise 就使用內(nèi)建的 Response class 對象來對響應(yīng)頭進行解析。

在這個階段,我們可以通過檢查響應(yīng)頭,來檢查 HTTP 狀態(tài)以確定請求是否成功,當(dāng)前還沒有響應(yīng)體(response body)。

如果 fetch 無法建立一個 HTTP 請求,例如網(wǎng)絡(luò)問題,亦或是請求的網(wǎng)址不存在,那么 promise 就會 reject。異常的 HTTP 狀態(tài),例如 404 或 500,不會導(dǎo)致出現(xiàn) error。

我們可以在 response 的屬性中看到 HTTP 狀態(tài):

  • ?status? —— HTTP 狀態(tài)碼,例如 200。
  • ?ok? —— 布爾值,如果 HTTP 狀態(tài)碼為 200-299,則為 ?true?。

例如:

let response = await fetch(url);

if (response.ok) { // 如果 HTTP 狀態(tài)碼為 200-299
  // 獲取 response body(此方法會在下面解釋)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

第二階段,為了獲取 response body,我們需要使用一個其他的方法調(diào)用。

Response 提供了多種基于 promise 的方法,來以不同的格式訪問 body:

  • ?response.text()? —— 讀取 response,并以文本形式返回 response,
  • ?response.json()? —— 將 response 解析為 JSON 格式,
  • ?response.formData()? —— 以 ?FormData? 對象(在 下一章 有解釋)的形式返回 response,
  • ?response.blob()? —— 以 Blob(具有類型的二進制數(shù)據(jù))形式返回 response,
  • ?response.arrayBuffer()? —— 以 ArrayBuffer(低級別的二進制數(shù)據(jù))形式返回 response,
  • 另外,?response.body? 是 ReadableStream 對象,它允許你逐塊讀取 body,我們稍后會用一個例子解釋它。

例如,我們從 GitHub 獲取最新 commits 的 JSON 對象:

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // 讀取 response body,并將其解析為 JSON 格式

alert(commits[0].author.login);

也可以使用純 promise 語法,不使用 await

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

要獲取響應(yīng)文本,可以使用 await response.text() 代替 .json()

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

let text = await response.text(); // 將 response body 讀取為文本

alert(text.slice(0, 80) + '...');

作為一個讀取為二進制格式的演示示例,讓我們 fetch 并顯示一張 “fetch” 規(guī)范 中的圖片(Blob 操作的有關(guān)內(nèi)容請見 Blob):

let response = await fetch('/article/fetch/logo-fetch.svg');

let blob = await response.blob(); // 下載為 Blob 對象

// 為其創(chuàng)建一個 <img>
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// 顯示它
img.src = URL.createObjectURL(blob);

setTimeout(() => { // 3 秒后將其隱藏
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);

重要:

我們只能選擇一種讀取 body 的方法。

如果我們已經(jīng)使用了 response.text() 方法來獲取 response,那么如果再用 response.json(),則不會生效,因為 body 內(nèi)容已經(jīng)被處理過了。

let text = await response.text(); // response body 被處理了
let parsed = await response.json(); // 失?。ㄒ呀?jīng)被處理過了)

Response header

Response header 位于 response.headers 中的一個類似于 Map 的 header 對象。

它不是真正的 Map,但是它具有類似的方法,我們可以按名稱(name)獲取各個 header,或迭代它們:

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// 獲取一個 header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// 迭代所有 header
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

Request header

要在 fetch 中設(shè)置 request header,我們可以使用 headers 選項。它有一個帶有輸出 header 的對象,如下所示:

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

……但是有一些我們無法設(shè)置的 header(詳見 forbidden HTTP headers):

  • ?Accept-Charset?, ?Accept-Encoding?
  • ?Access-Control-Request-Headers?
  • ?Access-Control-Request-Method?
  • ?Connection?
  • ?Content-Length?
  • ?Cookie?, ?Cookie2?
  • ?Date?
  • ?DNT?
  • ?Expect?
  • ?Host?
  • ?Keep-Alive?
  • ?Origin?
  • ?Referer?
  • ?TE?
  • ?Trailer?
  • ?Transfer-Encoding?
  • ?Upgrade?
  • ?Via?
  • ?Proxy-*?
  • ?Sec-*?

這些 header 保證了 HTTP 的正確性和安全性,所以它們僅由瀏覽器控制。

POST 請求

要創(chuàng)建一個 POST 請求,或者其他方法的請求,我們需要使用 fetch 選項:

  • ?method? —— HTTP 方法,例如 ?POST?,
  • ?body? —— request body,其中之一:
    • 字符串(例如 JSON 編碼的),
    • ?FormData? 對象,以 ?multipart/form-data? 形式發(fā)送數(shù)據(jù),
    • ?Blob?/?BufferSource? 發(fā)送二進制數(shù)據(jù),
    • URLSearchParams,以 ?x-www-form-urlencoded? 編碼形式發(fā)送數(shù)據(jù),很少使用。

JSON 形式是最常用的。

例如,下面這段代碼以 JSON 形式發(fā)送 ?user? 對象:

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

請注意,如果請求的 body 是字符串,則 Content-Type 會默認(rèn)設(shè)置為 text/plain;charset=UTF-8。

但是,當(dāng)我們要發(fā)送 JSON 時,我們會使用 headers 選項來發(fā)送 application/json,這是 JSON 編碼的數(shù)據(jù)的正確的 Content-Type

發(fā)送圖片

我們同樣可以使用 Blob 或 BufferSource 對象通過 fetch 提交二進制數(shù)據(jù)。

例如,這里有一個 <canvas>,我們可以通過在其上移動鼠標(biāo)來進行繪制。點擊 “submit” 按鈕將圖片發(fā)送到服務(wù)器:

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
        method: 'POST',
        body: blob
      });

      // 服務(wù)器給出確認(rèn)信息和圖片大小作為響應(yīng)
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

請注意,這里我們沒有手動設(shè)置 Content-Type header,因為 Blob 對象具有內(nèi)建的類型(這里是 image/png,通過 toBlob 生成的)。對于 Blob 對象,這個類型就變成了 Content-Type 的值。

可以在不使用 async/await 的情況下重寫 submit() 函數(shù),像這樣:

function submit() {
  canvasElem.toBlob(function(blob) {
    fetch('/article/fetch/post/image', {
      method: 'POST',
      body: blob
    })
      .then(response => response.json())
      .then(result => alert(JSON.stringify(result, null, 2)))
  }, 'image/png');
}

總結(jié)

典型的 fetch 請求由兩個 ?await? 調(diào)用組成:

let response = await fetch(url, options); // 解析 response header
let result = await response.json(); // 將 body 讀取為 json

或者以 promise 形式:

fetch(url, options)
  .then(response => response.json())
  .then(result => /* process result */)

響應(yīng)的屬性:

  • ?response.status? —— response 的 HTTP 狀態(tài)碼,
  • ?response.ok? —— HTTP 狀態(tài)碼為 200-299,則為 ?true?。
  • ?response.headers? —— 類似于 Map 的帶有 HTTP header 的對象。

獲取 response body 的方法:

  • ?response.text()? —— 讀取 response,并以文本形式返回 response,
  • ?response.json()? —— 將 response 解析為 JSON 對象形式,
  • ?response.formData()? —— 以 ?FormData? 對象(?multipart/form-data? 編碼,參見下一章)的形式返回 response,
  • ?response.blob()? —— 以 Blob(具有類型的二進制數(shù)據(jù))形式返回 response,
  • ?response.arrayBuffer()? —— 以 ArrayBuffer(低級別的二進制數(shù)據(jù))形式返回 response。

到目前為止我們了解到的 fetch 選項:

  • ?method? —— HTTP 方法,
  • ?headers? —— 具有 request header 的對象(不是所有 header 都是被允許的)
  • ?body? —— 要以 ?string?,?FormData?,?BufferSource?,?Blob? 或 ?UrlSearchParams? 對象的形式發(fā)送的數(shù)據(jù)(request body)。

在下一章,我們將會看到更多 ?fetch? 的選項和用例。

任務(wù)


從 GitHub fetch 用戶信息

創(chuàng)建一個異步函數(shù) getUsers(names),該函數(shù)接受 GitHub 登錄名數(shù)組作為輸入,查詢 GitHub 以獲取有關(guān)這些用戶的信息,并返回 GitHub 用戶數(shù)組。

帶有給定 USERNAME 的用戶信息的 GitHub 網(wǎng)址是:https://api.github.com/users/USERNAME。

沙箱中有一個測試用例。

重要的細(xì)節(jié):

  1. 對每一個用戶都應(yīng)該有一個 ?fetch? 請求。
  2. 請求不應(yīng)該相互等待。以便能夠盡快獲取到數(shù)據(jù)。
  3. 如果任何一個請求失敗了,或者沒有這個用戶,則函數(shù)應(yīng)該返回 ?null? 到結(jié)果數(shù)組中。

打開帶有測試的沙箱。


解決方案

要獲取一個用戶,我們需要:fetch('https://api.github.com/users/USERNAME').

如果響應(yīng)的狀態(tài)碼是 200,則調(diào)用 .json() 來讀取 JS 對象。

否則,如果 fetch 失敗,或者響應(yīng)的狀態(tài)碼不是 200,我們只需要向結(jié)果數(shù)組返回 null 即可。

代碼如下:

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

請注意:.then 調(diào)用緊跟在 fetch 后面,這樣,當(dāng)我們收到響應(yīng)時,它不會等待其他的 fetch,而是立即開始讀取 .json()。

如果我們使用 await Promise.all(names.map(name => fetch(...))),并在 results 上調(diào)用 .json() 方法,那么它將會等到所有 fetch 都獲取到響應(yīng)數(shù)據(jù)才開始解析。通過將 .json() 直接添加到每個 fetch 中,我們就能確保每個 fetch 在收到響應(yīng)時都會立即開始以 JSON 格式讀取數(shù)據(jù),而不會彼此等待。

這個例子表明,即使我們主要使用 async/await,低級別的 Promise API 仍然很有用。

使用沙箱的測試功能打開解決方案。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號