Javascript Fetch

2023-02-17 10:57 更新

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

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

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

……所有這些都沒有重新加載頁(yè)面!

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

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

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

基本語法:

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

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

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

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

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

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

如果 fetch 無法建立一個(gè) HTTP 請(qǐng)求,例如網(wǎng)絡(luò)問題,亦或是請(qǐng)求的網(wǎng)址不存在,那么 promise 就會(huì) reject。異常的 HTTP 狀態(tài),例如 404 或 500,不會(huì)導(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(此方法會(huì)在下面解釋)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

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

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

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

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

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) + '...');

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

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

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

// 為其創(chuàng)建一個(gè) <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(),則不會(huì)生效,因?yàn)?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 中的一個(gè)類似于 Map 的 header 對(duì)象。

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

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

// 獲取一個(gè) 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 選項(xiàng)。它有一個(gè)帶有輸出 header 的對(duì)象,如下所示:

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 請(qǐng)求

要?jiǎng)?chuàng)建一個(gè) POST 請(qǐng)求,或者其他方法的請(qǐng)求,我們需要使用 fetch 選項(xiàng):

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

JSON 形式是最常用的。

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

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

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

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

發(fā)送圖片

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

例如,這里有一個(gè) <canvas>,我們可以通過在其上移動(dòng)鼠標(biāo)來進(jìn)行繪制。點(diǎn)擊 “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>

請(qǐng)注意,這里我們沒有手動(dòng)設(shè)置 Content-Type header,因?yàn)?nbsp;Blob 對(duì)象具有內(nèi)建的類型(這里是 image/png,通過 toBlob 生成的)。對(duì)于 Blob 對(duì)象,這個(gè)類型就變成了 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 請(qǐng)求由兩個(gè) ?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 的對(duì)象。

獲取 response body 的方法:

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

到目前為止我們了解到的 fetch 選項(xiàng):

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

在下一章,我們將會(huì)看到更多 ?fetch? 的選項(xiàng)和用例。

任務(wù)


從 GitHub fetch 用戶信息

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

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

沙箱中有一個(gè)測(cè)試用例。

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

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

打開帶有測(cè)試的沙箱。


解決方案

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

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

否則,如果 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;
}

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

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

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

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


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)