使用 Fetch API

Fetch API 提供了一個 JavaScript 介面,用於進行 HTTP 請求和處理響應。

Fetch 是 XMLHttpRequest 的現代替代品:與使用回撥的 XMLHttpRequest 不同,Fetch 是基於 Promise 的,並與現代 Web 的功能整合,例如 Service Workers跨域資源共享 (CORS)

使用 Fetch API,您可以透過呼叫 fetch() 來發出請求,該函式在 windowworker 上下文中都可用。您可以向它傳遞一個 Request 物件或一個包含要獲取的 URL 的字串,以及一個可選引數來配置請求。

fetch() 函式返回一個 Promise,該 Promise 在收到表示伺服器響應的 Response 物件時完成。然後,您可以透過呼叫響應上的相應方法來檢查請求狀態並以各種格式(包括文字和 JSON)提取響應體。

下面是一個使用 fetch() 從伺服器檢索 JSON 資料的最小函式

js
async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const result = await response.json();
    console.log(result);
  } catch (error) {
    console.error(error.message);
  }
}

我們宣告一個包含 URL 的字串,然後呼叫 fetch(),傳遞 URL 且不帶任何額外選項。

fetch() 函式會在某些錯誤時拒絕 Promise,但如果伺服器響應的是錯誤狀態(如 404),則不會拒絕:因此我們還要檢查響應狀態,如果不是 OK 則丟擲錯誤。

否則,我們透過呼叫 Responsejson() 方法將響應體內容獲取為 JSON,並記錄其一個值。請注意,與 fetch() 本身一樣,json() 是非同步的,所有其他訪問響應體內容的方法也是非同步的。

在本頁的其餘部分,我們將更詳細地探討此過程的不同階段。

發出請求

要發出請求,請呼叫 fetch(),並傳入

  1. 要獲取的資源的定義。這可以是以下任何一種
    • 包含 URL 的字串
    • 一個物件,例如 URL 的例項,它有一個 字串化器,可以生成包含 URL 的字串
    • 一個 Request 例項
  2. 可選地,一個包含用於配置請求的選項的物件。

在本節中,我們將介紹一些最常用的選項。要閱讀所有可用選項,請參閱 fetch() 參考頁面。

設定方法

預設情況下,fetch() 發出 GET 請求,但您可以使用 method 選項來使用不同的請求方法

js
const response = await fetch("https://example.org/post", {
  method: "POST",
  // …
});

如果 mode 選項設定為 no-cors,則 method 必須是 GETPOSTHEAD 之一。

設定請求體

請求體是請求的有效負載:它是客戶端傳送到伺服器的內容。您不能在 GET 請求中包含請求體,但它對於向伺服器傳送內容的請求很有用,例如 POSTPUT 請求。例如,如果要將檔案上傳到伺服器,您可能會發出 POST 請求並將檔案作為請求體包含在內。

要設定請求體,請將其作為 body 選項傳遞

js
const response = await fetch("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  // …
});

您可以提供以下任何型別的例項作為請求體

其他物件使用其 toString() 方法轉換為字串。例如,您可以使用 URLSearchParams 物件對錶單資料進行編碼(有關更多資訊,請參閱設定請求頭

js
const response = await fetch("https://example.org/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
  },
  // Automatically converted to "username=example&password=password"
  body: new URLSearchParams({ username: "example", password: "password" }),
  // …
});

請注意,與響應體一樣,請求體也是流,發出請求會讀取流,因此如果請求包含請求體,您不能兩次發出它

js
const request = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
});

const response1 = await fetch(request);
console.log(response1.status);

// Will throw: "Body has already been consumed."
const response2 = await fetch(request);
console.log(response2.status);

相反,您需要在傳送請求之前建立請求的克隆

js
const request1 = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
});

const request2 = request1.clone();

const response1 = await fetch(request1);
console.log(response1.status);

const response2 = await fetch(request2);
console.log(response2.status);

有關更多資訊,請參閱鎖定和受干擾的流

設定請求頭

請求頭向伺服器提供有關請求的資訊:例如,在 POST 請求中,Content-Type 頭告訴伺服器請求體的格式。

要設定請求頭,請將它們分配給 headers 選項。

您可以在此處傳遞包含 header-name: header-value 屬性的物件字面量

js
const response = await fetch("https://example.org/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ username: "example" }),
  // …
});

或者,您可以構造一個 Headers 物件,使用 Headers.append() 向該物件新增請求頭,然後將 Headers 物件分配給 headers 選項

js
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const response = await fetch("https://example.org/post", {
  method: "POST",
  headers: myHeaders,
  body: JSON.stringify({ username: "example" }),
  // …
});

與使用普通物件相比,Headers 物件提供了一些額外的輸入淨化。例如,它將請求頭名稱標準化為小寫,從請求頭值中去除前導和尾隨空格,並阻止設定某些請求頭。許多請求頭由瀏覽器自動設定,不能由指令碼設定:這些被稱為禁止請求頭。如果 mode 選項設定為 no-cors,則允許設定的請求頭集將進一步受限。

在 GET 請求中傳送資料

GET 請求沒有請求體,但您仍然可以透過將其作為查詢字串附加到 URL 來向伺服器傳送資料。這是將表單資料傳送到伺服器的常用方法。您可以使用 URLSearchParams 對資料進行編碼,然後將其附加到 URL 來完成此操作

js
const params = new URLSearchParams();
params.append("username", "example");

// GET request sent to https://example.org/login?username=example
const response = await fetch(`https://example.org/login?${params}`);

發出跨域請求

請求是否可以跨域發出由 RequestInit.mode 選項的值決定。這可以採用以下三個值之一:corssame-originno-cors

  • 對於 fetch 請求,mode 的預設值為 cors,這意味著如果請求是跨域的,它將使用 跨域資源共享 (CORS) 機制。這意味著

    • 如果請求是簡單請求,則請求將始終傳送,但伺服器必須響應正確的 Access-Control-Allow-Origin 頭,否則瀏覽器不會與呼叫者共享響應。
    • 如果請求不是簡單請求,則瀏覽器將傳送預檢請求以檢查伺服器是否理解 CORS 並允許該請求,並且只有在伺服器響應預檢請求並帶有適當的 CORS 頭時,實際請求才會被髮送。
  • mode 設定為 same-origin 會完全禁止跨域請求。

  • mode 設定為 no-cors 會停用跨域請求的 CORS。這會限制可以設定的請求頭,並將方法限制為 GET、HEAD 和 POST。響應是不透明的,這意味著它的請求頭和請求體對 JavaScript 不可用。大多數情況下,網站不應使用 no-cors:它的主要應用是某些 Service Worker 用例。

有關更多詳細資訊,請參閱 RequestInit.mode 的參考文件。

包含憑據

在 Fetch API 的上下文中,憑據是隨請求傳送的額外資料,伺服器可以使用這些資料來驗證使用者。以下所有項都被視為憑據

預設情況下,憑據僅包含在同源請求中。要自定義此行為,以及控制瀏覽器是否遵守任何 Set-Cookie 響應頭,請設定 credentials 選項,該選項可以採用以下三個值之一

  • omit:從不在請求中傳送憑據或在響應中包含憑據。
  • same-origin(預設):僅傳送和包含同源請求的憑據。
  • include:始終包含憑據,即使是跨域請求。

請注意,如果 cookie 的 SameSite 屬性設定為 StrictLax,則即使 credentials 設定為 include,cookie 也不會跨站點發送。

在跨域請求中包含憑據可能會使網站容易受到 CSRF 攻擊,因此即使 credentials 設定為 include,伺服器也必須透過在其響應中包含 Access-Control-Allow-Credentials 請求頭來同意包含它們。此外,在這種情況下,伺服器必須在 Access-Control-Allow-Origin 響應頭中明確指定客戶端的來源(即不允許使用 *)。

這意味著如果 credentials 設定為 include 且請求是跨域的,那麼

  • 如果請求是簡單請求,則請求將隨憑據一起傳送,但伺服器必須設定 Access-Control-Allow-CredentialsAccess-Control-Allow-Origin 響應頭,否則瀏覽器將向呼叫者返回網路錯誤。如果伺服器確實設定了正確的請求頭,則響應(包括憑據)將交付給呼叫者。

  • 如果請求不是簡單請求,則瀏覽器將傳送不帶憑據的預檢請求,並且伺服器必須設定 Access-Control-Allow-CredentialsAccess-Control-Allow-Origin 響應頭,否則瀏覽器將向呼叫者返回網路錯誤。如果伺服器確實設定了正確的請求頭,則瀏覽器將繼續傳送實際請求(包括憑據),並將實際響應(包括憑據)交付給呼叫者。

建立 Request 物件

Request() 建構函式接受與 fetch() 本身相同的引數。這意味著您不必將選項傳遞給 fetch(),而是可以將相同的選項傳遞給 Request() 建構函式,然後將該物件傳遞給 fetch()

例如,我們可以使用如下程式碼透過將選項傳遞給 fetch() 來發出 POST 請求

js
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const response = await fetch("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  headers: myHeaders,
});

但是,我們可以將其重寫為將相同的引數傳遞給 Request() 建構函式

js
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");

const myRequest = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
  headers: myHeaders,
});

const response = await fetch(myRequest);

這也意味著您可以從另一個請求建立請求,同時使用第二個引數更改其某些屬性

js
async function post(request) {
  try {
    const response = await fetch(request);
    const result = await response.json();
    console.log("Success:", result);
  } catch (error) {
    console.error("Error:", error);
  }
}

const request1 = new Request("https://example.org/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ username: "example1" }),
});

const request2 = new Request(request1, {
  body: JSON.stringify({ username: "example2" }),
});

post(request1);
post(request2);

取消請求

要使請求可取消,請建立一個 AbortController,並將其 AbortSignal 分配給請求的 signal 屬性。

要取消請求,請呼叫控制器的 abort() 方法。fetch() 呼叫將拒絕 Promise 並丟擲 AbortError 異常。

js
const controller = new AbortController();

const fetchButton = document.querySelector("#fetch");
fetchButton.addEventListener("click", async () => {
  try {
    console.log("Starting fetch");
    const response = await fetch("https://example.org/get", {
      signal: controller.signal,
    });
    console.log(`Response: ${response.status}`);
  } catch (e) {
    console.error(`Error: ${e}`);
  }
});

const cancelButton = document.querySelector("#cancel");
cancelButton.addEventListener("click", () => {
  controller.abort();
  console.log("Canceled fetch");
});

如果在 fetch() 呼叫完成之後但在響應體被讀取之前中止請求,則嘗試讀取響應體將拒絕並丟擲 AbortError 異常。

js
async function get() {
  const controller = new AbortController();
  const request = new Request("https://example.org/get", {
    signal: controller.signal,
  });

  const response = await fetch(request);
  controller.abort();
  // The next line will throw `AbortError`
  const text = await response.text();
  console.log(text);
}

處理響應

一旦瀏覽器從伺服器收到響應狀態和請求頭(可能在響應體本身收到之前),fetch() 返回的 Promise 將使用 Response 物件完成。

檢查響應狀態

fetch() 返回的 Promise 會在某些錯誤(例如網路錯誤或錯誤的方案)時拒絕。但是,如果伺服器響應錯誤(例如 404),則 fetch() 會返回一個 Response,因此我們必須在讀取響應體之前檢查狀態。

Response.status 屬性告訴我們數字狀態碼,而 Response.ok 屬性在狀態在 200 範圍內時返回 true

一種常見的模式是檢查 ok 的值,如果為 false 則丟擲錯誤

js
async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }
    // …
  } catch (error) {
    console.error(error.message);
  }
}

檢查響應型別

響應具有 type 屬性,可以是以下之一

  • basic:請求是同源請求。
  • cors:請求是跨域 CORS 請求。
  • opaque:請求是使用 no-cors 模式發出的跨域簡單請求。
  • opaqueredirect:請求將 redirect 選項設定為 manual,並且伺服器返回了重定向狀態

型別決定了響應的可能內容,如下所示

  • 基本響應排除禁止響應頭名稱列表中的響應頭。

  • CORS 響應僅包含CORS 安全列表響應頭列表中的響應頭。

  • 不透明響應和不透明重定向響應的 status0,空請求頭列表和 null 響應體。

檢查請求頭

與請求一樣,響應也具有 headers 屬性,它是一個 Headers 物件,其中包含根據響應型別進行的排除後暴露給指令碼的任何響應頭。

一個常見的用例是在嘗試讀取正文之前檢查內容型別

js
async function fetchJSON(request) {
  try {
    const response = await fetch(request);
    const contentType = response.headers.get("content-type");
    if (!contentType || !contentType.includes("application/json")) {
      throw new TypeError("Oops, we haven't got JSON!");
    }
    // Otherwise, we can read the body as JSON
  } catch (error) {
    console.error("Error:", error);
  }
}

讀取響應體

Response 介面提供了許多方法,以各種不同的格式檢索整個請求體內容

這些都是非同步方法,返回一個 Promise,該 Promise 將以請求體內容完成。

在此示例中,我們獲取影像並將其讀取為 Blob,然後我們可以使用它來建立物件 URL

js
const image = document.querySelector("img");

const url = "flowers.jpg";

async function setImage() {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }
    const blob = await response.blob();
    const objectURL = URL.createObjectURL(blob);
    image.src = objectURL;
  } catch (e) {
    console.error(e);
  }
}

如果響應體格式不正確,該方法將丟擲異常:例如,如果您在無法解析為 JSON 的響應上呼叫 json()

流式傳輸響應體

請求和響應體實際上是 ReadableStream 物件,每當您讀取它們時,您都在流式傳輸內容。這對記憶體效率很有利,因為瀏覽器不必在呼叫者使用 json() 等方法檢索整個響應之前將其緩衝到記憶體中。

這也意味著呼叫者可以在收到內容時逐步處理內容。

例如,考慮一個 GET 請求,它獲取一個大文字檔案並以某種方式處理它,或將其顯示給使用者

js
const url = "https://www.example.org/a-large-file.txt";

async function fetchText(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const text = await response.text();
    console.log(text);
  } catch (e) {
    console.error(e);
  }
}

如果像上面那樣使用 Response.text(),我們必須等到整個檔案都收到後才能處理其中的任何部分。

如果我們改為流式傳輸響應,我們可以在從網路接收到資料塊時處理它們

js
const url = "https://www.example.org/a-large-file.txt";

async function fetchTextAsStream(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const stream = response.body.pipeThrough(new TextDecoderStream());
    for await (const value of stream) {
      console.log(value);
    }
  } catch (e) {
    console.error(e);
  }
}

在此示例中,我們非同步迭代流,在每個資料塊到達時進行處理。

請注意,當您像這樣直接訪問主體時,您會得到響應的原始位元組,並且必須自己進行轉換。在這種情況下,我們呼叫 ReadableStream.pipeThrough() 將響應透過 TextDecoderStream 管道傳輸,後者將 UTF-8 編碼的主體資料解碼為文字。

逐行處理文字檔案

在下面的示例中,我們獲取一個文字資源並逐行處理它,使用正則表示式查詢行尾。為簡單起見,我們假設文字是 UTF-8,並且不處理 fetch 錯誤

js
async function* makeTextFileLineIterator(fileURL) {
  const response = await fetch(fileURL);
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

  let { value: chunk = "", done: readerDone } = await reader.read();

  const newline = /\r?\n/g;
  let startIndex = 0;

  while (true) {
    const result = newline.exec(chunk);
    if (!result) {
      if (readerDone) break;
      const remainder = chunk.slice(startIndex);
      ({ value: chunk, done: readerDone } = await reader.read());
      chunk = remainder + (chunk || "");
      startIndex = newline.lastIndex = 0;
      continue;
    }
    yield chunk.substring(startIndex, result.index);
    startIndex = newline.lastIndex;
  }

  if (startIndex < chunk.length) {
    // Last line didn't end in a newline char
    yield chunk.substring(startIndex);
  }
}

async function run(urlOfFile) {
  for await (const line of makeTextFileLineIterator(urlOfFile)) {
    processLine(line);
  }
}

function processLine(line) {
  console.log(line);
}

run("https://www.example.org/a-large-file.txt");

鎖定和受干擾的流

請求和響應體是流的後果是

  • 如果已使用 ReadableStream.getReader() 將讀取器附加到流,則該流被鎖定,並且其他任何東西都無法讀取該流。
  • 如果已從流中讀取任何內容,則該流被干擾,並且其他任何東西都無法從該流中讀取。

這意味著不可能多次讀取相同的響應(或請求)體

js
async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }

    const result1 = await response.json();
    const result2 = await response.json(); // will throw
  } catch (error) {
    console.error(error.message);
  }
}

如果您確實需要多次讀取請求體,則必須在讀取請求體之前呼叫 Response.clone()

js
async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response1 = await fetch(url);
    if (!response1.ok) {
      throw new Error(`Response status: ${response1.status}`);
    }

    const response2 = response1.clone();

    const result1 = await response1.json();
    const result2 = await response2.json();
  } catch (error) {
    console.error(error.message);
  }
}

這是使用 Service Worker 實現離線快取時的一種常見模式。Service Worker 希望將響應返回給應用程式,但也希望快取響應。因此,它克隆響應,返回原始響應,並快取克隆。

js
async function cacheFirst(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open("MyCache_1");
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    return Response.error();
  }
}

self.addEventListener("fetch", (event) => {
  if (precachedResources.includes(url.pathname)) {
    event.respondWith(cacheFirst(event.request));
  }
});

另見