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

另见