跳到主要内容

Fetch API

Fetch API 能够执行 XMLHttpRequest 对象的所有任务,但更容易使用,接口也更现代化,能够在Web 工作线程等现代 Web 工具中使用。XMLHttpRequest 可以选择异步,而 Fetch API 则必须是异步。Fetch API 是 WHATWG 的一个“活标准”(living standard),用规范原文说,就是“Fetch 标准定义请求、响应,以及绑定二者的流程:获取(fetch)”。

Fetch API 本身是使用 JavaScript 请求资源的优秀工具,同时这个 API 也能够应用在服务线程(service worker)中,提供拦截、重定向和修改通过 fetch() 生成的请求接口。

基本用法

fetch() 方法是暴露在全局作用域中的,包括主页面执行线程、模块和工作线程。调用这个方法, 浏览器就会向给定 URL 发送请求。

分派请求

fetch() 只有一个必需的参数 input。多数情况下,这个参数是要获取资源的 URL。URL 的格式(相对路径、绝对路径等)的解释与 XHR 对象一样。

请求完成、资源可用时,期约会解决为一个 Response 对象。这个对象是 API 的封装,可以通过它取得相应资源。获取资源要使用这个对象的属性和方法,掌握响应的情况并将负载转换为有用的形式,如下所示:

fetch('http://127.0.0.1:8080/data').then(response => {
console.log(response); // Response { type: "basic", url: ... }
});

读取响应

读取响应内容的最简单方式是取得纯文本格式的内容,这要用到 text()方法。这个方法返回一个期约,会解决为取得资源的完整内容:

fetch('http://127.0.0.1:8080/data')
.then(response => response.text())
.then(value => {
// 获取服务器端响应的文本数据
console.log(value);
});

使用 json() 方法,可以使返回的数据是 JSON 格式。

fetch('http://127.0.0.1:8080/data')
.then(response => response.json())
.then(value => {
// 获取服务器端响应的JSON格式数据
console.log(value);
});

处理状态码和请求失败

Fetch API 支持通过 Response 的 status(状态码)和 statusText(状态文本)属性检查响应状态。成功获取响应的请求通常会产生值为 200 的状态码,如下所示:

fetch('http://127.0.0.1:8080/data').then(response => {
console.log(response.status); // 200
console.log(response.statusText); // OK
});

// 请求不存在的资源通常会产生值为404的状态码:
fetch('http://127.0.0.1:8080/not-found').then(response => {
console.log(response.status); // 404
console.log(response.statusText); // Not Found
});

// 请求的 URL 如果抛出服务器错误会产生值为 500 的状态码:
fetch('http://127.0.0.1:8080/throw-server-error').then(response => {
console.log(response.status); // 500
console.log(response.statusText); // Internal Server Error
});

在前面这几个例子中,虽然请求可能失败(如状态码为 404、500),但都只执行了期约的解决处理函数。事实上,只要服务器返回了响应,fetch() 期约都会解决。这个行为是合理的:系统级网络协议已经成功完成消息的一次往返传输。

至于真正的“成功”请求,则需要在处理响应时再定义。通常状态码为 200 时就会被认为成功了,其他情况可以被认为未成功。

因为服务器没有响应而导致浏览器超时,这样真正的 fetch() 失败会导致期约被拒绝:

自定义选项

只使用 URL 时,fetch() 会发送 GET 请求,只包含最低限度的请求头。要进一步配置如何发送请求,需要传入可选的第二个参数 init 对象。init 对象有如下属性:

body

指定使用请求体时请求体的内容必须是 BlobBufferSourceFormDataURLSearchParamsReadableStreamString 的实例

cache

用于控制浏览器与 HTTP缓存的交互。要跟踪缓存的重定向,请求的 redirect 属性值必须是"follow",而且必须符合同源策略限制。必须是下列值之一

  • default
  • fetch() 返回命中的有效缓存。不发送请求
  • 命中无效(stale)缓存会发送条件式请求。如果响应已经改变,则更新缓存的值。然后 fetch() 返回缓存的值
  • 未命中缓存会发送请求,并缓存响应。然后 fetch() 返回响应
  • no-store
  • 浏览器不检查缓存,直接发送请求
  • 不缓存响应,直接通过 fetch()返回
  • reload
  • 浏览器不检查缓存,直接发送请求
  • 缓存响应,再通过 fetch()返回
  • no-cache
  • 无论命中有效缓存还是无效缓存都会发送条件式请求。如果响应已经改变,则更新缓存的值。然后 fetch()返回缓存的值
  • force-cache
  • 无论命中有效缓存还是无效缓存都通过 fetch()返回。不发送请求
  • only-if-cached
  • 只在请求模式为 same-origin 时使用缓存

credentials

用于指定在外发请求中如何包含 cookie。与 XMLHttpRequest 的 withCredentials 标签类似 必须是下列字符串值之一

  • omit:不发送
  • same-origin(默认值):只在请求 URL 与发送 fetch()请求的页面同源时发送 cookie
  • include:无论同源还是跨源都包含 cookie

headers

用于指定请求头部,必须是 Headers 对象实例或包含字符串格式键/值对的常规对象

默认值为不包含键/值对的 Headers 对象。这不意味着请求不包含任何头部,浏览器仍然会随请求发送一些头部。虽然这些头部对 JavaScript 不可见,但浏览器的网络检查器可以观察到

integrity

用于强制子资源完整性

必须是包含子资源完整性标识符的字符串,默认为空字符串。

keepalive

用于指示浏览器允许请求存在时间超出页面生命周期。适合报告事件或分析,比如页面在 fetch() 请求后很快卸载。设置 keepalive 标志的 fetch() 请求可用于替代 Navigator.sendBeacon() 必须是布尔值,默认为 false.

method

用于指定 HTTP 请求方法, 有如下:

  • GET(默认值)
  • POST
  • PUT
  • PATCH
  • DELETE
  • HEAD: 与 GET 类似,但服务器只返回头部信息,不返回实际数据。通常用于检查资源是否已更改,以减少数据传输。
  • OPTIONS: 获取有关资源的信息,或者关于服务器的功能的信息。通常用于 CORS(跨域资源共享)预检请求。
  • CONNECT: 用于将服务器作为代理连接,通常用于安全隧道(例如,在通过 SSL/TLS 连接时建立代理)。
  • TRACE: 用于在目的服务器上执行一个请求诊断。然而,由于安全性问题,大多数现代浏览器都禁用了 TRACE 方法

mode

用于指定请求模式。这个模式决定来自跨源请求的响应是否有效,以及客户端可以读取多少响应。违反这里指定模式的请求会抛出错误,必须是下列字符串值之一:

  • cors(默认值):允许遵守 CORS 协议的跨源请求。
  • no-cors: 允许不需要发送预检请求的跨源请求。
  • same-origin: 任何跨源请求都不允许发送
  • navigate:用于支持 HTML 导航,只在文档间导航时使用。

redirect

用于指定如何处理重定向响应(状态码为 301、302、303、307 或 308),必须是下列字符串值之一:

  • follow(默认值):跟踪重定向请求,以最终非重定向 URL 的响应作为最终响应
  • error:重定向请求会抛出错误
  • manual:不跟踪重定向请求,而是返回 opaqueredirect 类型的响应,同时仍然暴露期望的重定向 URL。允许以手动方式跟踪重定向

referrer

用于指定 HTTP 的 Referer 头部的内容

referrerPolicy

用于指定 HTTP 的 Referer 头部

signal

用于支持通过 AbortController 中断进行中的 fetch()请求,必须是 AbortSignal 的实例,默认为未关联控制器的 AbortSignal 实例

注意

自定义选项不完整,带补充

常见 Fetch 请求模式

与 XMLHttpRequest 一样,fetch() 既可以发送数据也可以接收数据。使用 init 对象参数,可以配置 fetch() 在请求体中发送各种序列化的数据。

发送 JSON 数据

let payload = JSON.stringify({
foo: 'bar',
});
let jsonHeaders = new Headers({
'Content-Type': 'application/json',
});
fetch('/send-me-json', {
method: 'POST',
body: payload,
headers: jsonHeaders,
});

在请求体中发送参数

let payload = 'foo=bar&baz=qux';
let paramHeaders = new Headers({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
});
fetch('/send-me-params', {
method: 'POST',
body: payload,
headers: paramHeaders,
});

发送文件

因为请求体支持 FormData 实现,所以 fetch() 也可以序列化并发送文件字段中的文件:

let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file']");
imageFormData.append('image', imageInput.files[0]);
fetch('/img-upload', {
method: 'POST',
body: imageFormData,
});
实现可以支持多个文件
let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file'][multiple]");
for (let i = 0; i < imageInput.files.length; ++i) {
imageFormData.append('image', imageInput.files[i]);
}
fetch('/img-upload', {
method: 'POST',
body: imageFormData,
});

加载 Blob 文件

Fetch API也能提供 Blob 类型的响应,而 Blob 又可以兼容多种浏览器 API。一种常见的做法是明确将图片文件加载到内存,然后将其添加到 HTML图片元素。为此,可以使用响应对象上暴露的 blob() 方法。这个方法返回一个期约,解决为一个 Blob 的实例。然后,可以将这个实例传给 URL.createObjectUrl()以生成可以添加给图片元素 src 属性的值:

const imageElement = document.querySelector('img');
fetch('my-image.png')
.then(response => response.blob())
.then(blob => {
imageElement.src = URL.createObjectURL(blob);
});

发送跨源请求

从不同的源请求资源,响应要包含 CORS 头部才能保证浏览器收到响应。没有这些头部,跨源请求会失败并抛出错误。

fetch('//cross-origin.com');
// TypeError: Failed to fetch
// No 'Access-Control-Allow-Origin' header is present on the requested resource.

如果代码不需要访问响应,也可以设置请求模式 no-cors 模式。此时响应的 type 属性值为 opaque,因此无法读取响应内容。这种方式适合发送探测请求或者将响应缓存起来供以后使用。

fetch('//cross-origin.com', { mode: 'no-cors' }).then(response =>
console.log(response.type),
);
// opaque

中断请求

Fetch API 支持通过 AbortController/AbortSignal 对中断请求。调用 AbortController.abort() 会中断所有网络传输,特别适合希望停止传输大型负载的情况。中断进行中的 fetch() 请求会导致返回的期约拒绝。

let abortController = new AbortController();
fetch('wikipedia.zip', { signal: abortController.signal }).catch(() =>
console.log('aborted!'),
);

// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10);
// 已经中断

Headers 对象

Headers 对象是所有外发请求和入站响应头部的容器。每个外发的 Request 实例都包含一个空的 Headers 实例,可以通过 Request.prototype.headers 访问,每个入站 Response 实例也可以通过 Response.prototype.headers 访问包含着响应头部的 Headers 对象。这两个属性都是可修改属性。 另外,使用 new Headers() 也可以创建一个新实例。

Headers 与 Map 的相似之处

Headers 对象与 Map 对象极为相似。这是合理的,因为 HTTP 头部本质上是序列化后的键/值对,它们的 JavaScript 表示则是中间接口。Headers 与 Map 类型都有 get()set()has()delete() 等实例方法,如下面的代码所示:

let h = new Headers();

// 设置键、更新键
h.set('foo', 'bar');

// 检查键,返回 boolean 值
console.log(h.has('foo')); // true

// 获取值
console.log(h.get('foo')); // bar

// 删除值
h.delete('foo');

Headers 和 Map 都可以使用一个可迭代对象来初始化,比如:

let seed = [['foo', 'bar']];
let h = new Headers(seed);
let m = new Map(seed);

console.log(h.get('foo')); // bar
console.log(m.get('foo')); // bar

而且,它们也都有相同的 keys()values()entries()迭代器接口:

let seed = [
['foo', 'bar'],
['baz', 'qux'],
];

let h = new Headers(seed);
let m = new Map(seed);

console.log(...h.keys()); // foo, baz
console.log(...m.keys()); // foo, baz

console.log(...h.values()); // bar, qux
console.log(...m.values()); // bar, qux

console.log(...h.entries()); // ['foo', 'bar'], ['baz', 'qux']
console.log(...m.entries()); // ['foo', 'bar'], ['baz', 'qux']

Headers 独有的特性

Headers 并不是与 Map 处处都一样。在初始化 Headers 对象时,也可以使用键/值对形式的对象,而 Map 则不可以:

let seed = { foo: 'bar' };

let h = new Headers(seed);
console.log(h.get('foo')); // bar

let m = new Map(seed);
// TypeError: object is not iterable

一个 HTTP 头部字段可以有多个值,而 Headers 对象通过 append() 方法支持添加多个值。在Headers 实例中还不存在的头部上调用 append() 方法相当于调用 set()。后续调用会以逗号为分隔符拼接多个值:

let h = new Headers();

h.append('foo', 'bar');
console.log(h.get('foo')); // "bar"

h.append('foo', 'baz');
console.log(h.get('foo')); // "bar, baz"

头部护卫

某些情况下,并非所有 HTTP 头部都可以被客户端修改,而 Headers 对象使用护卫来防止不被允许的修改。不同的护卫设置会改变 set()append()delete() 的行为。违反护卫限制会抛出TypeError。

Headers 实例会因来源不同而展现不同的行为,它们的行为由护卫来控制。JavaScript 可以决定 Headers 实例的护卫设置。下表列出了不同的护卫设置和每种设置对应的行为。

护 卫适用情形限 制
none在通过构造函数创建 Headers 实例时激活
request在通过构造函数初始化 Request 对象,且 mode 值为非 no-cors 时激活不允许修改禁止修改的头部
request-no-cors在通过构造函数初始化 Request 对象,且 mode 值为 no-cors 时激活不允许修改非简单头部
response在通过构造函数初始化 Response 对象时激活不允许修改禁止修改的响应头部
immutable在通过 error()redirect() 静态方法初始化 Response 对象时激活不允许修改任何头部

Request 对象

顾名思义,Request 对象是获取资源请求的接口。这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。

创建 Request 对象

可以通过构造函数初始化 Request 对象。为此需要传入一个 input 参数,一般是 URL:

let r = new Request('http://127.0.0.1');
console.log(r);

// 其他属性为默认值,若需要修改提供 init 对象,如下
// Request {
// bodyUsed: false
// cache: "default"
// credentials: "same-origin"
// destination: ""
// headers: Headers {}
// integrity: ""
// keepalive: false
// method: "GET"
// mode: "cors"
// redirect: "follow"
// referrer: "about:client"
// referrerPolicy: ""
// signal: AbortSignal {aborted: false, onabort: null}
// url: "http://127.0.0.1/"
// }

Request 构造函数也接收第二个参数——一个 init 对象。这个 init 对象与前面介绍的 fetch() 的 init 对象一样。没有在 init 对象中涉及的值则会使用默认值:

console.log(new Request('http://127.0.0.1', { method: 'POST' }));

// Request {
// ……
// method: "POST"
// }

克隆 Request 对象

Fetch API 提供了两种不太一样的方式用于创建 Request 对象的副本:使用 Request 构造函数和使用 clone() 方法。

使用 Request 构造函数
let r1 = new Request('http://127.0.0.1');
// 如果再传入 init 对象,则 init 对象的值会覆盖源对象中同名的值
let r2 = new Request(r1);

console.log(r2.url); // http://127.0.0.1

这种克隆方式并不总能得到一模一样的副本。最明显的是,第一个请求的请求体会被标记为“已使用”:

// 前提是请求必须携带请求体,才会有 bodyUsed 属性变化
let r1 = new Request('http://127.0.0.1', { method: 'POST', body: 'foobar' });
let r2 = new Request(r1);
console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false

如果源对象与创建的新对象不同源,则 referrer 属性会被清除。此外,如果源对象的 mode 为 navigate,则会被转换为 same-origin。

第二种克隆 Request 对象的方式是使用 clone() 方法,这个方法会创建一模一样的副本,任何值都不会被覆盖。与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”:

使用 clone() 方法
let r1 = new Request('http://127.0.0.1', { method: 'POST', body: 'foobar' });
let r2 = r1.clone();

console.log(r1.url); // http://127.0.0.1/
console.log(r2.url); // http://127.0.0.1/

console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

如果请求对象的 bodyUsed 属性为 true(即请求体已被读取),那么上述任何一种方式都不能用来创建这个对象的副本。在请求体被读取之后再克隆会导致抛出 TypeError。

let r = new Request('http://127.0.0.1', {
method: 'POST',
body: 'foobar',
});
// 调用text()方法。就会导致 bodyUsed 为 true
r.text();

r.clone();
new Request(r);

在 fetch() 中使用 Request 对象

fetch() 和 Request 构造函数拥有相同的函数签名并不是巧合。在调用 fetch() 时,可以传入已经创建好的 Request 实例而不是 URL。与 Request 构造函数一样,传给 fetch() 的 init 对象会覆盖传入请求对象的值:

let r = new Request('http://127.0.0.1');

// 向 http://127.0.0.1 发送 GET 请求
fetch(r);
// 向 http://127.0.0.1 发送 POST 请求
fetch(r, { method: 'POST' });

fetch() 会在内部克隆传入的 Request 对象。与克隆 Request 一样,fetch() 也不能拿请求体已经用过的 Request 对象来发送请求:

let r = new Request('http://127.0.0.1', { method: 'POST', body: 'foobar' });
r.text();
fetch(r);
// TypeError: Cannot construct a Request with a Request object that has already been used.

关键在于,通过 fetch 使用 Request 会将请求体标记为已使用。也就是说,有请求体的 Request 只能在一次 fetch 中使用。(不包含请求体的请求不受此限制。)演示如下:

let r = new Request('http://127.0.0.1', { method: 'POST', body: 'foobar' });
fetch(r);
fetch(r);
// TypeError: Cannot construct a Request with a Request object that has already been used.

// 不包含 body 请求的请求
let r2 = new Request('http://127.0.0.1');
fetch(r2); // 成功请求
fetch(r2); // 成功请求

要想基于包含请求体的相同 Request 对象多次调用 fetch(),必须在第一次发送 fetch() 请求前调用 clone()

let r = new Request('http://127.0.0.1', { method: 'POST', body: '请求体数据' });
// 原理是使用clone克隆,这种方法不会将任何请求的请求体标记为“已使用”:
fetch(r.clone());
fetch(r.clone());
fetch(r);

Response 对象

顾名思义,Response 对象是获取资源响应的接口。这个接口暴露了响应的相关信息,也暴露了使用响应体的不同方式。

创建 Response 对象

可以通过构造函数初始化 Response 对象且不需要参数。此时响应实例的属性均为默认值,因为它 并不代表实际的 HTTP 响应:

let r = new Response();
console.log(r);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: "OK"
// type: "default"
// url: ""
// }

Response 构造函数接收一个可选的 body 参数。这个 body 可以是 null,等同于 fetch() 参数 init 中的 body。还可以接收一个可选的 init 对象,这个对象可以包含下表所列的键和值。

headers必须是 Headers 对象实例或包含字符串键/值对的常规对象实例, 默认为空的 Headers 对象
status表示 HTTP 响应状态码的整数,默认为 200
statusText表示 HTTP 响应状态的字符串, 默认为空字符串

可以像下面这样使用 body 和 init 来构建 Response 对象:

let r = new Response('foobar', {
status: 418,
statusText: "I'm a teapot",
});

console.log(r);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 418
// statusText: "I'm a teapot"
// type: "default"
// url: ""
// }

大多数情况下,产生 Response 对象的主要方式是调用 fetch(),它返回一个最后会解决为 Response 对象的期约,这个 Response 对象代表实际的 HTTP 响应。下面的代码展示了这样得到的 Response 对象:

fetch('http://127.0.0.1').then(response => {
console.log(response);
});
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: "OK"
// type: "basic"
// url: "http://127.0.0.1"
// }

Response 类还有两个用于生成 Response 对象的静态方法:Response.redirect()Response.error()。前者接收一个 URL 和一个重定向状态码(301、302、303、307 或 308),返回重定向的 Response对象:

console.log(Response.redirect('http://127.0.0.1', 301));
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 301
// statusText: ""
// type: "default"
// url: ""
// }

// 提供的状态码必须对应重定向,否则会抛出错误:
Response.redirect('http://127.0.0.1', 200);
// RangeError: Failed to execute 'redirect' on 'Response': Invalid status code

另一个静态方法 Response.error() 用于产生表示网络错误的 Response 对象(网络错误会导致 fetch() 期约被拒绝)。

console.log(Response.error());
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 0
// statusText: ""
// type: "error"
// url: ""
// }

读取响应状态信息

Response 对象包含一组只读属性,描述了请求完成后的状态,如下表所示。

属性
headers响应包含的 Headers 对象
ok布尔值,表示 HTTP 状态码的含义。200~299 的状态码返回 true,其他状态码返回 false
redirected布尔值,表示响应是否至少经过一次重定向
status整数,表示响应的 HTTP 状态码
statusText字符串,包含对 HTTP 状态码的正式描述。这个值派生自可选的 HTTP Reason-Phrase 字段,因此如果服务器以 Reason-Phrase 为由拒绝响应,这个字段可能是空字符串
type字符串,包含响应类型。可能是下列字符串值之一: basiccorserroropaqueopaqueredirect
url包含响应 URL 的字符串。对于重定向响应,这是最终的 URL,非重定向响应就是它产生的

克隆 Response 对象

克隆 Response 对象的主要方式是使用 clone() 方法,这个方法会创建一个一模一样的副本,不会覆盖任何值。这样不会将任何请求的请求体标记为已使用:

let r1 = new Response('foobar');
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

如果响应对象的 bodyUsed 属性为 true(即响应体已被读取),则不能再创建这个对象的副本。在响应体被读取之后再克隆会导致抛出 TypeError。

let r = new Response('foobar');

r.text(); // 设置 bodyUsed 为 true

r.clone();
// TypeError: Failed to execute 'clone' on 'Response': Response body is already used

有响应体的 Response 对象只能读取一次。(不包含响应体的 Response 对象不受此限制。)比如:

let r = new Response('foobar');
r.text().then(console.log); // foobar

r.text().then(console.log);
// TypeError: Failed to execute 'text' on 'Response': body stream is locked

要多次读取包含响应体的同一个 Response 对象,必须在第一次读取前调用 clone()

let r = new Response('foobar');

r.clone().text().then(console.log); // foobar
r.clone().text().then(console.log); // foobar
r.text().then(console.log); // foobar

此外,通过创建带有原始响应体的 Response 实例,可以执行伪克隆操作。关键是这样不会把第一个 Response 实例标记为已读,而是会在两个响应之间共享:

let r1 = new Response('foobar');
let r2 = new Response(r1.body); // r2的实例传入了r1的响应体

console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

r1.text().then(console.log); // foobar
console.log(r2.bodyUsed); // true

Request、Response 及 Body 混入

Request 和 Response 都使用了 Fetch API 的 Body 混入,以实现两者承担有效载荷的能力。这个混入为两个类型提供了只读的 body 属性(实现为 ReadableStream)、只读的 bodyUsed 布尔值(表示 body 流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种 JavaScript 对象类型。

通常,将 Request 和 Response 主体作为流来使用主要有两个原因。一个原因是有效载荷的大小可能会导致网络延迟,另一个原因是流 API 本身在处理有效载荷方面是有优势的。除此之外,最好是一次性获取资源主体。

Body 混入提供了 5 个方法,用于将 ReadableStream 转存到缓冲区的内存里,将缓冲区转换为某种 JavaScript 对象类型,以及通过期约来产生结果。在解决之前,期约会等待主体流报告完成及缓冲被解析。这意味着客户端必须等待响应的资源完全加载才能访问其内容。

Body.text()

Body.text() 方法返回期约,解决为将缓冲区转存得到的 UTF-8 格式字符串。

fetch('http://127.0.0.1')
.then(response => response.text())
.then(console.log); // 字符串

Body.json()

Body.json() 方法返回期约,解决为将缓冲区转存得到的 JSON。

fetch('http://127.0.0.1/foo.json')
.then(response => response.json())
.then(console.log); // {"key": "value"}

Body.formData()

浏览器可以将 FormData 对象序列化/反序列化为主体。

在通过 HTTP 传送时,WebKit 浏览器会将其序列化为下列内容:

------WebKitFormBoundarydR9Q2kOzE6nbN7eR
Content-Disposition: form-data; name="foo"
bar
------WebKitFormBoundarydR9Q2kOzE6nbN7eR--

Body.formData() 方法返回期约,解决为将缓冲区转存得到的 FormData 实例。

fetch('http://127.0.0.1/form-data')
.then(response => response.formData())
.then(formData => console.log(formData.get('foo')));
// bar
展示在 Request 对象上使用 Body.formData()
let myFormData = new FormData();
myFormData.append('foo', 'bar');
let request = new Request('http://127.0.0.1', {
method: 'POST',
body: myFormData,
});
request.formData().then(formData => console.log(formData.get('foo')));
// bar

Body.arrayBuffer()

有时候,可能需要以原始二进制格式查看和修改主体。为此,可以使用 Body.arrayBuffer() 将主体内容转换为 ArrayBuffer 实例。Body.arrayBuffer() 方法返回期约,解决为将缓冲区转存得到的 ArrayBuffer 实例。

fetch('http://127.0.0.1')
.then(response => response.arrayBuffer())
.then(console.log);
// ArrayBuffer(...) {}
展示在 Request 对象上使用 Body.arrayBuffer()
let request = new Request('http://127.0.0.1', {
method: 'POST',
body: 'abcdefg',
});
// 以整数形式打印二进制编码的字符串
request.arrayBuffer().then(buffer => console.log(new Int8Array(buffer)));
// Int8Array(7) [97, 98, 99, 100, 101, 102, 103]

Body.blob()

有时候,可能需要以原始二进制格式使用主体,不用查看和修改。为此,可以使用 Body.blob() 将主体内容转换为 Blob 实例。Body.blob() 方法返回期约,解决为将缓冲区转存得到的 Blob 实例。

fetch('http://127.0.0.1')
.then(response => response.blob())
.then(console.log);
// Blob(...) {size:..., type: "..."}
展示在 Request 对象上使用 Body.blob()
let request = new Request('http://127.0.0.1', {
method: 'POST',
body: 'abcdefg',
});
request.blob().then(console.log);
// Blob(7) {size: 7, type: "text/plain;charset=utf-8"}

一次性流

因为 Body 混入是构建在 ReadableStream 之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调用一次,再次调用就会抛出错误。

即使是在读取流的过程中,所有这些方法也会在它们被调用时给 ReadableStream 加锁,以阻止其他读取器访问:

fetch('http://127.0.0.1').then(response =>
response.blob().then(() => response.blob()),
);
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked

fetch('http://127.0.0.1').then(response => {
response.blob(); // 第一次调用给流加锁
response.blob(); // 第二次调用再次加锁会失败
});
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked

作为 Body 混入的一部分,bodyUsed 布尔值属性表示 ReadableStream 是否已摄受(disturbed),意思是读取器是否已经在流上加了锁。这不一定表示流已经被完全读取。下面的代码演示了这个属性:

let request = new Request('http://127.0.0.1', {
method: 'POST',
body: 'foobar',
});
let response = new Response('foobar');

console.log(request.bodyUsed); // false
console.log(response.bodyUsed); // false

request.text().then(console.log); // foobar
response.text().then(console.log); // foobar

console.log(request.bodyUsed); // true
console.log(response.bodyUsed); // true

使用 ReadableStream 主体

JavaScript 编程逻辑很多时候会将访问网络作为原子操作,比如请求是同时创建和发送的,响应数据也是以统一的格式一次性暴露出来的。这种约定隐藏了底层的混乱,让涉及网络的代码变得很清晰。

从 TCP/IP 角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。接收端点会为此分配内存,并将收到的块写入内存。Fetch API 通过 ReadableStream 支持在这些块到达时就实时读取和操作这些数据。

正如Stream API所定义的,ReadableStream暴露了getReader()方法,用于产生ReadableStreamDefaultReader,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是 Uint8Array。

下面的代码调用了读取器的 read() 方法,把最早可用的块打印了出来:

fetch('https://fetch.spec.whatwg.org/')
.then(response => response.body)
.then(body => {
let reader = body.getReader();
console.log(reader); // ReadableStreamDefaultReader {}
reader.read().then(console.log);
});

// { value: Uint8Array{}, done: false }

在随着数据流的到来取得整个有效载荷,可以像下面这样递归调用 read() 方法:

fetch('https://fetch.spec.whatwg.org/')
.then(response => response.body)
.then(body => {
let reader = body.getReader();
function processNextChunk({ value, done }) {
if (done) {
return;
}
console.log(value);
return reader.read().then(processNextChunk);
}
return reader.read().then(processNextChunk);
});

// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...

异步函数非常适合这样的 fetch() 操作。可以通过使用 async/await 将上面的递归调用打平:

fetch('https://fetch.spec.whatwg.org/')
.then(response => response.body)
.then(async function (body) {
let reader = body.getReader();
while (true) {
let { value, done } = await reader.read();
if (done) {
break;
}
console.log(value);
}
});

// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...

通过将异步逻辑包装到一个生成器函数中,还可以进一步简化代码。而且,这个实现通过支持只读取部分流也变得更稳健。如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续操作:

async function* streamGenerator(stream) {
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
yield value;
}
} finally {
reader.releaseLock();
}
}

fetch('https://fetch.spec.whatwg.org/')
.then(response => response.body)
.then(async function (body) {
for await (chunk of streamGenerator(body)) {
console.log(chunk);
}
});

在这些例子中,当读取完 Uint8Array 块之后,浏览器会将其标记为可以被垃圾回收。对于需要在不连续的内存中连续检查大量数据的情况,这样可以节省很多内存空间。

缓冲区的大小,以及浏览器是否等待缓冲区被填充后才将其推到流中,要根据 JavaScript 运行时的实现。浏览器会控制等待分配的缓冲区被填满,同时会尽快将缓冲区数据(有时候可能未填充数据)发送到流。

不同浏览器中分块大小可能不同,这取决于带宽和网络延迟。此外,浏览器如果决定不等待网络,也可以将部分填充的缓冲区发送到流。最终,我们的代码要准备好处理以下情况:

  • 不同大小的 Uint8Array 块;
  • 部分填充的 Uint8Array 块;
  • 块到达的时间间隔不确定。

默认情况下,块是以 Uint8Array 格式抵达的。因为块的分割不会考虑编码,所以会出现某些值作为多字节字符被分散到两个连续块中的情况。手动处理这些情况是很麻烦的,但很多时候可以使用Encoding API 的可插拔方案。

要将 Uint8Array 转换为可读文本,可以将缓冲区传给 TextDecoder,返回转换后的值。通过设置 stream: true,可以将之前的缓冲区保留在内存,从而让跨越两个块的内容能够被正确解码:

let decoder = new TextDecoder();

async function* streamGenerator(stream) {
const reader = stream.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
yield value;
}
} finally {
reader.releaseLock();
}
}

fetch('https://fetch.spec.whatwg.org/')
.then(response => response.body)
.then(async function (body) {
for await (chunk of streamGenerator(body)) {
console.log(decoder.decode(chunk, { stream: true }));
}
});

// <!doctype html><html lang="en"> ...
// whether a <a data-link-type="dfn" href="#concept-header" ...
// result to <var>rangeValue</var>. ...
// ...

因为可以使用 ReadableStream 创建 Response 对象,所以就可以在读取流之后,将其通过管道导入另一个流。然后在这个新流上再使用 Body 的方法,如 text()。这样就可以随着流的到达实时检查和操作流内容。下面的代码展示了这种双流技术:

fetch('https://fetch.spec.whatwg.org/')
.then(response => response.body)
.then(body => {
const reader = body.getReader();
// 创建第二个流
return new ReadableStream({
async start(controller) {
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
// 将主体流的块推到第二个流
controller.enqueue(value);
}
} finally {
controller.close();
reader.releaseLock();
}
},
});
})
.then(secondaryStream => new Response(secondaryStream))
.then(response => response.text())
.then(console.log);
// <!doctype html><html lang="en"><head><meta charset="utf-8"> ...