2022 年 4 月 14 日

擷取

JavaScript 可以向伺服器發送網路請求,並在需要時載入新的資訊。

例如,我們可以使用網路請求來

  • 提交訂單,
  • 載入使用者資訊,
  • 接收伺服器發送的最新更新,
  • …等等。

…而且無需重新載入網頁!

有一個統稱「AJAX」(簡稱Asynchronous JavaScript And XML)用於表示 JavaScript 的網路請求。不過我們不必使用 XML:這個術語來自於舊時代,這就是為什麼會有這個字詞。你可能已經聽過這個術語了。

有多種方法可以發送網路請求並從伺服器取得資訊。

fetch() 方法既現代又多功能,因此我們將從此開始。舊瀏覽器不支援此方法(可以使用 polyfill),但現代瀏覽器支援得很好。

基本語法為

let promise = fetch(url, [options])
  • url – 要存取的 URL。
  • options – 選擇性參數:方法、標頭等。

若沒有 options,這會是一個簡單的 GET 要求,下載 url 的內容。

瀏覽器會立即開始要求,並傳回一個承諾,呼叫程式碼應使用該承諾來取得結果。

取得回應通常是一個兩階段的流程。

首先,fetch 傳回的 promise 會在伺服器回應標頭時,使用內建 Response 類別的物件來解析。

在此階段,我們可以檢查 HTTP 狀態,查看是否成功,檢查標頭,但還沒有主體。

如果 fetch 無法進行 HTTP 要求(例如網路問題或沒有此網站),承諾會被拒絕。異常的 HTTP 狀態,例如 404 或 500,不會造成錯誤。

我們可以在回應屬性中看到 HTTP 狀態

  • status – HTTP 狀態碼,例如 200。
  • ok – 布林值,如果 HTTP 狀態碼為 200-299,則為 true

例如

let response = await fetch(url);

if (response.ok) { // if HTTP-status is 200-299
  // get the response body (the method explained below)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

其次,要取得回應主體,我們需要使用額外的呼叫方法。

Response 提供多個基於承諾的方法,以各種格式存取主體

  • response.text() – 讀取回應並傳回為文字,
  • response.json() – 將回應解析為 JSON,
  • response.formData() – 將回應傳回為 FormData 物件(在 下一章 中說明),
  • response.blob() – 將回應傳回為 Blob(帶有類型的二進位資料),
  • response.arrayBuffer() – 將回應傳回為 ArrayBuffer(二進位資料的低階表示),
  • 此外,response.bodyReadableStream 物件,它允許您逐塊讀取主體,我們稍後會看到一個範例。

例如,讓我們從 GitHub 取得一個包含最新提交的 JSON 物件

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

let commits = await response.json(); // read response body and parse as JSON

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

或者,使用純粹的承諾語法,不使用 await

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

要取得回應文字,請使用 await response.text(),而非 .json()

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

let text = await response.text(); // read response body as text

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

作為二進位格式讀取的展示範例,我們來擷取並顯示 Blob 章節)

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

let blob = await response.blob(); // download as Blob object

// create <img> for it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// show it
img.src = URL.createObjectURL(blob);

setTimeout(() => { // hide after three seconds
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);
重要

我們只能選擇一種主體讀取方法。

如果我們已使用 response.text() 取得回應,則 response.json() 將無法運作,因為主體內容已處理完畢。

let text = await response.text(); // response body consumed
let parsed = await response.json(); // fails (already consumed)

回應標頭

回應標頭在 response.headers 中以類似 Map 的標頭物件提供。

它並非完全是 Map,但它具有類似的方法,可依名稱取得個別標頭或反覆處理它們

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

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

// iterate over all headers
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

要求標頭

若要在 fetch 中設定要求標頭,我們可以使用 headers 選項。它有一個包含傳出標頭的物件,如下所示

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

…不過,有一份 禁止的 HTTP 標頭 清單,我們無法設定

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

這些標頭可確保 HTTP 正確且安全,因此它們由瀏覽器獨家控制。

POST 要求

若要執行 POST 要求或使用其他方法的請求,我們需要使用 fetch 選項

  • method – HTTP 方法,例如 POST
  • body – 請求主體,其中之一為
    • 字串(例如 JSON 編碼),
    • FormData 物件,以 multipart/form-data 提交資料,
    • Blob/BufferSource,用於傳送二進位資料,
    • URLSearchParams,用於以 x-www-form-urlencoded 編碼提交資料,很少使用。

大多數時候都使用 JSON 格式。

例如,以下程式碼會將 user 物件提交為 JSON

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 標頭預設設定為 text/plain;charset=UTF-8

不過,由於我們要傳送 JSON,因此我們使用 headers 選項來傳送 application/json,這是 JSON 編碼資料正確的 Content-Type

傳送圖片

我們也可以使用 BlobBufferSource 物件,透過 fetch 傳送二進制資料。

在這個範例中,有一個 <canvas>,我們可以在上面移動滑鼠來繪圖。點選「提交」按鈕會將圖片傳送至伺服器

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

      // the server responds with confirmation and the image size
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

請注意,這裡我們沒有手動設定 Content-Type 標頭,因為 Blob 物件有一個內建類型(這裡是 image/png,由 toBlob 產生)。對於 Blob 物件,該類型會變成 Content-Type 的值。

submit() 函式可以改寫成不使用 async/await 的形式,如下所示

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

摘要

一個典型的 fetch 要求包含兩個 await 呼叫

let response = await fetch(url, options); // resolves with response headers
let result = await response.json(); // read body as json

或者,不使用 await

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

回應屬性

  • response.status – 回應的 HTTP 程式碼,
  • response.ok – 如果狀態是 200-299,則為 true
  • response.headers – 包含 HTTP 標頭的類似 Map 的物件。

取得回應主體的方法

  • response.text() – 將回應傳回為文字,
  • response.json() – 將回應解析為 JSON 物件,
  • response.formData() – 將回應傳回為 FormData 物件(multipart/form-data 編碼,請參閱下一章),
  • response.blob() – 將回應傳回為 Blob(帶有類型的二進位資料),
  • response.arrayBuffer() – 將回應傳回為 ArrayBuffer(低階二進制資料),

目前為止的 Fetch 選項

  • method – HTTP 方法,
  • headers – 包含要求標頭的物件(並非任何標頭都允許),
  • body – 要傳送的資料(要求主體),可以是 stringFormDataBufferSourceBlobUrlSearchParams 物件。

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

作業

建立一個非同步函式 getUsers(names),它會取得一個 GitHub 登入陣列,從 GitHub 取得使用者,並傳回一個 GitHub 使用者陣列。

給定 USERNAME 的 GitHub 使用者資訊網址為:https://api.github.com/users/USERNAME

沙盒中有一個測試範例。

重要事項

  1. 每個使用者都應該有一個 fetch 要求。
  2. 要求不應該互相等待。這樣資料才能盡快抵達。
  3. 如果任何要求失敗,或沒有該使用者,函式應該在結果陣列中傳回 null

開啟一個包含測試的沙盒。

要取得使用者,我們需要:fetch('https://api.github.com/users/USERNAME')

如果回應的狀態是 200,請呼叫 .json() 來讀取 JS 物件。

否則,如果 fetch 失敗,或回應的狀態非 200,我們只會在結果陣列中傳回 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 呼叫直接附加到 fetch,因此當我們有回應時,它不會等待其他擷取,而是立即開始讀取 .json()

如果我們使用 await Promise.all(names.map(name => fetch(...))),並對結果呼叫 .json(),那麼它會等待所有擷取回應。透過將 .json() 直接新增到每個 fetch,我們確保個別擷取會在不等待彼此的情況下開始讀取資料為 JSON。

這是低階 Promise API 範例,即使我們主要使用 async/await,它仍然很有用。

在沙盒中開啟有測試的解決方案。

教學課程地圖

留言

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