使用 Fetch API

The Fetch API provides a JavaScript interface for making HTTP requests and processing the responses.

Fetch 是 XMLHttpRequest 的现代替代品:与使用回调的 XMLHttpRequest 不同,Fetch 是基于 Promise 的,并与现代 Web 的功能集成在一起,例如 服务工作者跨域资源共享 (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 json = await response.json();
    console.log(json);
  } 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", {
  body: JSON.stringify({ username: "example" }),
  // ...
});

您可以将正文提供为以下任何类型的实例

请注意,与响应正文一样,请求正文是流,发出请求将读取流,因此如果请求包含正文,您就不能两次发出该请求

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

有关更多信息,请参阅 锁定和干扰的流

设置报头

请求报头向服务器提供有关请求的信息:例如,Content-Type 报头告诉服务器请求正文的格式。许多报头由浏览器自动设置,脚本无法设置:这些称为 禁止的报头名称

要设置请求报头,请将它们分配给 headers 选项。

您可以在此处传递一个包含 header-name: header-value 属性的对象字面量

js
const response = await fetch("https://example.org/post", {
  headers: {
    "Content-Type": "application/json",
  },
  // ...
});

或者,您可以构造一个 Headers 对象,使用 Headers.append() 向该对象添加报头,然后将 Headers 对象分配给 headers 选项

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

const response = await fetch("https://example.org/post", {
  headers: myHeaders,
  // ...
});

如果 mode 选项设置为 no-cors,则您只能设置 CORS 安全列表中的请求报头

发出 POST 请求

我们可以结合 methodbodyheaders 选项来发出 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,
});

发出跨域请求

是否可以发出跨域请求取决于 mode 选项的值。它可以取三个值之一:corsno-corssame-origin

  • 默认情况下,mode 设置为 cors,这意味着如果请求是跨域的,则它将使用 跨域资源共享 (CORS) 机制。这意味着
    • 如果请求是 简单请求,则始终会发送请求,但服务器必须以正确的 Access-Control-Allow-Origin 报头进行响应,否则浏览器不会与调用者共享响应。
    • 如果请求不是简单请求,则浏览器将发送 预检请求 以检查服务器是否了解 CORS 并允许该请求,并且只有在服务器以适当的 CORS 报头响应预检请求时才会发送实际请求。
  • mode 设置为 same-origin 将完全禁止跨域请求。
  • mode 设置为 no-cors 意味着请求必须是简单请求,这会限制可以设置的报头,并将方法限制为 GETHEADPOST

包含凭据

凭据是 Cookie、TLS 客户端证书或包含用户名和密码的身份验证报头。

要控制浏览器是否发送凭据,以及浏览器是否遵守任何 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()调用将拒绝承诺,并引发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()返回的承诺将使用Response对象完成。

检查响应状态

fetch()返回的承诺将在某些错误(例如网络错误或错误的方案)上被拒绝。但是,如果服务器以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,服务器返回了重定向状态

类型决定了响应的可能内容,如下所示

检查头

与请求一样,响应也具有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,该承诺将使用主体内容完成。

在此示例中,我们获取图像并将其读取为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,并且不处理获取错误

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();
  chunk = chunk || "";

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

  while (true) {
    const result = newline.exec(chunk);
    if (!result) {
      if (readerDone) break;
      const remainder = chunk.substr(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 json1 = await response.json();
    const json2 = 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 json1 = await response1.json();
    const json2 = await response2.json();
  } catch (error) {
    console.error(error.message);
  }
}

这是在使用服务工作者实现离线缓存时常见的模式。服务工作者希望将响应返回给应用程序,但也希望缓存响应。因此,它克隆响应,返回原始响应,并缓存克隆

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

另请参阅