Fetch API能执行XMLHttpRequest对象的所有任务,能在Web工作线程等现代Web工具中使用,是WHATWG的一个“活标准”(living standard),用规范原文说,就是“Fetch标准定义请求、响应,以及绑定二者的流程:获取(fetch)”。
Fetch API本身是使用JavaScript请求资源的优秀工具,同时这个API也能够应用在服务线程(service worker)中,提供拦截、重定向和修改通过fetch()生成的请求接口。
XMLHttpRequest可以选择异步,而Fetch API则必须是异步。
24.5.1 基本用法
fetch()方法暴露在全局作用域中,包括主页面执行线程、模块和工作线程。
调用这个方法,浏览器会向给定URL发送请求。
1.分派请求
fetch()只有一个必需的参数input。多数情况下,这个参数是要获取资源的URL。返回一个期约。
let r = fetch('/bar');console.log(r) // Promise <pending>
URL的格式(相对路径、绝对路径等)的解释与XHR对象一样。
请求完成、资源可用时,期约会解决为一个Response对象。
这个对象是API的封装,可通过它取得相应资源。
获取资源要用这个对象的属性和方法,掌握响应的情况,并将负载转换为有用的形式。
如下所示:
fetch('bar.text').then((response) => {console.log(response);});// Response { type: 'basic', url: ...}
2.读取响应
读取响应内容的最简单方式是:取得纯文本格式的内容,要用text()方法。返回一个期约,会解决为取得资源的完整内容。
fetch('bar.text').then((response) => {response.text().then((data) => {console.log(data)});});// bar.text的内容
内容的结构通常是打平的:
fetch('bar.text').then((response) => response.text()).then((data) => console.log(data));// bar.text的内容
3.处理状态码和请求失败
Fetch API支持通过Response的status(状态码)和statusText(状态文本)属性检查响应状态。
fetch('/bar').then((response) => {console.log(response.status);console.log(response.statusText);})// 成功获取响应的请求通常会产生值为200的状态码:// 第一行打印:200,第二行打印:OK// 请求不存在的资源通常会产生值为404的状态码:// 第一行打印:404,第二行打印:NotFound// 请求的URL如果抛出服务器错误会产生值为500的状态码:// 第一行打印:500,第二行打印:InternalServerError
可显式设置fetch()在遇到重定向时的行为(本章后面会介绍),但默认行为是:跟随重定向并返回状态码不是300~399的响应。
跟随重定向时,响应对象的redirected属性会被设置为true,而状态码仍然是200。
在前面这几个例子中,虽然请求可能失败(如状态码为500),但都只执行了期约的解决处理函数。
事实上,只要服务器返回了响应,fetch()期约都会解决。
这个行为是合理的:系统级网络协议已经成功完成消息的一次往返传输。
至于真正的“成功”请求,则需要在处理响应时再定义。
通常状态码为200时就会被认为成功,其他情况可以被认为未成功。
为区分这两种情况,可在状态码非200~299时检查Response对象的ok属性。
因为服务器没有响应而导致浏览器超时,这样真正的fetch()失败会导致期约被拒绝:
fetch('/hangs-forever').then((response) => {console.log(response);}, (err) => {console.log(err);});// (浏览器超时后)// TypeError: "NetworkError when attempting to fetch resource"
违反CORS、无网络连接、HTTPS错配及其他浏览器/网络策略问题都会导致期约被拒绝。
可通过url属性检查通过fetch()发送请求时使用的完整URL
4.自定义选项
只使用URL时,fetch()会发送GET请求,只包含最低限度的请求头。
要进一步配置如何发送请求,需要传入可选的第二个参数init对象。
init对象要按照下表中的键/值进行填充。(见书中图片)
24.5.2 常见Fetch请求模式
与XMLHttpRequest一样,fetch()既可发送数据也可接收数据。
使用init对象参数,可配置fetch()在请求体中发送各种序列化的数据。
1.发送JSON数据
可以像下面这样发送简单JSON字符串:
let payload = JSON.stringify({foo: 'bar'});let jsonHeaders = new Headers({'Content-Type': 'application/json'});fetch('/send-me-json', {method: 'POST', // 发送请求体时必须使用一种HTTP方法body: payload,headers: jsonHeaders});
2.在请求体中发送参数
因为请求体支持任意字符串值,所以可以通过它发送请求参数:
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', // 发送请求体时必须使用一种HTTP方法body: payload,headers: paramHeaders});
3.发送文件
因为请求体支持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});// 这个fetch()实现可以支持多个文件:let imageFormData = new FormData();let imageInput = document.querySelector("input[type='file']");for (let i = 0; i < imageInput.files.length; ++i) {imageFormData.append('image', imageInput.files[i]);}fetch('/img/upload', {method: 'POST',body: imageFormData});
4.加载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);});
5.发送跨源请求
从不同的源请求资源,响应要包含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,因此无法读取响应内容。// 这种方式适合发送探测请求或者将响应缓存起来供以后使用。const imageElement = document.querySelector('img');fetch('//cross-origin.com', { method: 'no-cors'}).then((response) => console.log(response.type));// opaque
6.中断请求
Fetch API支持通过AbortController/AbortSignal对中断请求。
调用AbortController. abort()会中断所有网络传输,特别适合希望停止传输大型负载的情况。
中断进行中的fetch()请求会导致包含错误的拒绝。
let abortController = new AbortController();fetch('wikipedia.zip', { signal: abortController.signal }).catch(() => console.log('中断请求!'));// 10毫秒后中断请求setTimeout(() => abortController.abort(), 10);// 已经中断
24.5.3 Headers对象
Headers对象是所有外发请求和入站响应头部的容器。
每个外发的Request实例都包含一个空的Headers实例,可通过Request.prototype.headers访问;
每个入站Response实例也可通过Response.prototype.headers访问包含着响应头部的Headers对象。
这两个属性都是可修改属性。
另外,使用new Headers()也可以创建一个新实例。
1.Headers与Map的相似之处
Headers对象与Map对象极为相似。这是合理的。
因为HTTP头部本质上是序列化后的键/值对,它们的JavaScript表示则是中间接口。
Headers与Map类型都有get()、set()、has()和delete()等实例方法。
set 设置键或更新值,has 检查键,get 获取值,delete 删除值
Headers和Map都可以使用一个可迭代对象来初始化
let seed = [['foo', 'bar']];let h = new Headers(seed);let m = new Map(seed);console.log(h.get('foo')); // barconsole.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()); // baz fooconsole.log(...m.keys()); // foo bazconsole.log(...h.values()); // qux barconsole.log(...m.values()); // bar quxconsole.log(...h.entries()); // (2) ["baz", "qux"] (2) ["foo", "bar"]console.log(...m.entries()); // (2) ["foo", "bar"] (2) ["baz", "qux"]
2.Headers独有的特性
在初始化Headers对象时,也可以使用键/值对形式的对象,而Map则不可以.
let seed = {foo: 'bar'};let h = new Headers(seed);let m = new Map(seed);console.log(h.get('foo')); // barconsole.log(m.get('foo'));// Uncaught TypeError: object is not iterable (cannot read property// Symbol(Symbol.iterator)) at new Map (<anonymous>)
一个HTTP头部字段可以有多个值,而Headers对象通过append()方法支持添加多个值。
在Headers实例中还不存在的头部上调用append()方法相当于调用set()。
后续调用会以逗号为分隔符拼接多个值。
let h = new Headers();h.append('foo', 'bar');console.log(h.get('foo')); // barh.append('foo', 'baz');console.log(h.get('foo')); // bar, baz
3.头部护卫
某些情况下,并非所有HTTP头部都可以被客户端修改,而Headers对象使用护卫来防止不被允许的修改。
不同的护卫设置会改变set()、append()和delete()的行为。违反护卫限制会抛出TypeError。
Headers实例会因来源不同而展现不同的行为,它们的行为由护卫来控制。
JavaScript可以决定Headers实例的护卫设置。
下表列出了不同的护卫设置和每种设置对应的行为:
24.5.4 Request对象
顾名思义,Request对象是获取资源请求的接口。
这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。
1.创建Request对象
可通过构造函数初始化Request对象。
为此需要传入一个input参数,一般是URL
let r = new Request('https://foo.com');console.log(r);// Request {...}
Request构造函数也接收第二个参数——一个init对象。
这个init对象与前面介绍的fetch()的init对象一样。
没有在init对象中涉及的值则会使用默认值。
// 用所有默认值创建Request对象console.log(new Request(''));// Request {// bodyUsed: false// cache: "default"// credentials: "same-origin"// destination: ""// headers: Headers {}// integrity: ""// isHistoryNavigation: false// keepalive: false// method: "GET"// mode: "cors"// redirect: "follow"// referrer: "about:client"// referrerPolicy: ""// signal: AbortSignal {aborted: false, onabort: null}// url: "file:///Users/xxx/Desktop/test.html"// }// 用指定的初始值创建Request对象console.log(new Request('https://foo.com', { method: 'POST' }));// Request {// bodyUsed: false// cache: "default"// credentials: "same-origin"// destination: ""// headers: Headers {}// integrity: ""// isHistoryNavigation: false// keepalive: false// method: "POST"// mode: "cors"// redirect: "follow"// referrer: "about:client"// referrerPolicy: ""// signal: AbortSignal {aborted: false, onabort: null}// url: "https://foo.com/"// }
2.克隆Request对象
Fetch API提供了两种不太一样的方式用于创建Request对象的副本:
使用Request构造函数和使用clone()方法。
将Request实例作为input参数传给Request构造函数,会得到该请求的一个副本;
若再传入init对象,则init对象的值会覆盖源对象中同名的值。
这种克隆方式并不总能得到一模一样的副本:
第一个请求的请求体会被标记为“已使用”。
若源对象与创建的新对象不同源,则referrer属性会被清除。
若源对象的mode为navigate,则会被转换为same-origin。
let r1 = new Request('https://foo.com');let r2 = new Request(r1);console.log(r2.url); // https://foo.comlet r1 = new Request('https://foo.com');let r2 = new Request(r1, {method: 'POST'});console.log(r1.method); // GETconsole.log(r2.method); // POSTlet r1 = new Request('https://foo.com', {method: 'POST', body: 'foobar'});let r2 = new Request(r1);console.log(r1.bodyUsed); // trueconsole.log(r2.bodyUsed); // false
第二种克隆Request对象的方式是使用clone()方法,这个方法会创建一模一样的副本,任何值都不会被覆盖。
与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”。
let r1 = new Request('https://foo.com', {method: 'POST', body: 'foobar'});let r2 = r1.clone();console.log(r1.url); // https://foo.comconsole.log(r2.url); // https://foo.comconsole.log(r1.bodyUsed); // falseconsole.log(r2.bodyUsed); // false
若请求对象的bodyUsed属性为true(即请求体已被读取),那么上述任何一种方式都不能用来创建这个对象的副本。在请求体被读取之后再克隆会导致抛出TypeError。
3.在fetch()中使用Request对象
fetch()和Request构造函数拥有相同的函数签名并不是巧合。
调用fetch()时,可传入已经创建好的Request实例而不是URL。
与Request构造函数一样,传给fetch()的init对象会覆盖传入请求对象的值。
fetch()会在内部克隆传入的Request对象。
与克隆Request一样,fetch()也不能拿请求体已经用过的Request对象来发送请求。
关键在于,通过fetch使用Request会将请求体标记为已使用。也就是说,有请求体的Request只能在一次fetch中使用。(不包含请求体的请求不受此限制。)
要想基于包含请求体的相同Request对象多次调用fetch(),必须在第一次发送fetch()请求前调用clone()
let r = new Request('https://foo.com');// 向foo.com发送GET请求fetch(r);// 向foo.com发送POST请求fetch(r, {method: 'POST'});let r = new Request('https://foo.com', {method: 'POST', body: 'foobar'});r.text();fetch(r);// TypeError: Cannot construct a Request with a Request object that has already been used.let r = new Request('https://foo.com', {method: 'POST', body: 'foobar'});fetch(r);fetch(r);// TypeError: Cannot construct a Request with a Request object that has already been used.let r = new Request('https://foo.com', {method: 'POST', body: 'foobar'});fetch(r.clone());fetch(r.clone());fetch(r);
24.5.5 Response对象
顾名思义,Response对象是获取资源响应的接口。
这个接口暴露了响应的相关信息,也暴露了使用响应体的不同方式。
1.创建Response对象
可通过构造函数初始化Response对象且不需要参数。此时响应实例的属性均为默认值,因为它并不代表实际的HTTP响应.
let r = new Response();console.log(r);// Response {// body: (...)// bodyUsed: false// headers: Headers {}// ok: true// redirected: false// status: 200// statusText: ""// type: "default"// url: ""// }
Response构造函数接收一个可选的body参数。
这个body可以是null,等同于fetch()参数init中的body。
还可接收一个可选的init对象,这个对象可以包含下表所列的键和值:
可以像下面这样使用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('https://foo.com/').then((response) => {console.log(response);});
Response类还有两个用于生成Response对象的静态方法:
Response.redirect()和Response.error()。
前者接收一个URL和一个重定向状态码(301、302、303、307或308),返回重定向的Response对象。
提供的状态码必须对应重定向,否则会抛出错误
另一个静态方法Response.error()用于产生表示网络错误的Response对象(网络错误会导致fetch()期约被拒绝)。
console.log(Response.redirect('重定向地址', 301));Response.redirect('重定向地址', 200);// 会报错console.log(Response.error());// Response {// body: (...)// bodyUsed: false// headers: Headers {}// ok: false// redirected: false// status: 0// statusText: ""// type: "error"// url: ""// }
2.读取响应状态信息
Response对象包含一组只读属性,描述了请求完成后的状态。
如下表所示:
3.克隆Response对象
克隆Response对象的主要方式是使用clone()方法。
这个方法会创建一个一模一样的副本,不会覆盖任何值。
这样不会将任何请求的请求体标记为已使用。
若响应对象的bodyUsed属性为true(即响应体已被读取),则不能再创建这个对象的副本。在响应体被读取之后再克隆会导致抛出TypeError。
有响应体的Response对象只能读取一次。(不包含响应体的Response对象不受此限制。)
要多次读取包含响应体的同一个Response对象,必须在第一次读取前调用clone()
let r1 = new Response('foobar');let r2 = r1.clone();console.log(r1.bodyUsed); // falseconsole.log(r2.bodyUsed); // falselet r = new Response('foobar');r.clone();// 没有错误r.text(); // 设置bodyUsed为truer.clone();// Uncaught TypeError: Failed to execute 'clone' on 'Response': Response body is already usedlet r = new Response('foobar');r.text().then(console.log); // foobarr.text().then(console.log);// Uncaught (in promise) TypeError: Failed to execute 'text' on 'Response': body stream already readlet r = new Response('foobar');r.clone().text().then(console.log);r.clone().text().then(console.log);r.text().then(console.log);// foobar 打印了3次
此外,通过创建带有原始响应体的Response实例,可以执行伪克隆操作。关键是这样不会把第一个Response实例标记为已读,而是会在两个响应之间共享
let r1 = new Response('foobar');let r2 = new Response(r1.body);console.log(r1.bodyUsed); // falseconsole.log(r2.bodyUsed); // falser2.text().then(console.log); // foobarr1.text().then(console.log);// Uncaught (in promise) TypeError: Failed to execute 'text' on 'Response': body stream already read
24.5.6 Request、Response及Body混入
Request和Response都使用了Fetch API的Body混入,以实现两者承担有效载荷的能力。
这个混入为两个类型提供了只读的body属性(实现为ReadableStream)、只读的bodyUsed布尔值(表示body流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种JavaScript对象类型。
通常,将Request和Response主体作为流来使用主要有两个原因:
一个原因是有效载荷的大小可能会导致网络延迟;
另一个原因是流API本身在处理有效载荷方面是有优势的。
除此之外,最好是一次性获取资源主体。
Body混入提供了5个方法,用于将ReadableStream转存到缓冲区的内存里,将缓冲区转换为某种JavaScript对象类型,以及通过期约来产生结果。在解决之前,期约会等待主体流报告完成及缓冲被解析。这意味着客户端必须等待响应的资源完全加载才能访问其内容。
1.Body.text()
Body.text()方法返回期约,解决为将缓冲区转存得到的UTF-8格式字符串。
下面的代码展示了在Response对象上使用Body.text():
以下代码展示了在Request对象上使用Body.text():
let request = new Request('https://foo.com', {method: 'POST', body: 'bbbbbbb'});request.text().then(console.log);// bbbbbbb
2.Body.json()
Body.json()方法返回期约,解决为将缓冲区转存得到的JSON。
下面的代码展示了在Response对象上使用Body.json():
以下代码展示了在Request对象上使用Body.json():
let request = new Request('https://foo.com',{method: 'POST', body: JSON.stringify({bar: 'baz'})});request.json().then(console.log);// {bar: "baz"}
3.Body.formData()
浏览器可以将FormData对象序列化/反序列化为主体。
例如,下面这个FormData实例:
let myFormData = new FormData();myFormData.append('foo', 'bar');
在通过HTTP传送时,WebKit浏览器会将其序列化为下列内容:
Body.formData()方法返回期约,解决为将缓冲区转存得到的FormData实例。
下面的代码展示了在Response对象上使用Body.formData():
以下代码展示了在Request对象上使用Body.formData():
let myFormData = new FormData();myFormData.append('foo', 'bar');let request = new Request('https://foo.com',{method: 'POST', body: myFormData});request.formData().then((formData) => console.log(formData.get('foo')));// bar
4.Body.arrayBuffer()
有时可能需要以原始二进制格式查看和修改主体。为此,可以使用Body.arrayBuffer()将主体内容转换为ArrayBuffer实例。
Body.arrayBuffer()方法返回期约,解决为将缓冲区转存得到的ArrayBuffer实例。
5.Body.blob()
有时可能需要以原始二进制格式使用主体,不用查看和修改。为此,可以使用Body.blob()将主体内容转换为Blob实例。
Body.blob()方法返回期约,解决为将缓冲区转存得到的Blob实例。
下面的代码展示了在Response对象上使用Body.blob():
以下代码展示了在Request对象上使用Body.blob():
let request = new Request('https://foo.com',{method: 'POST', body: 'abcdefg'});request.blob().then(console.log);// Blob {size: 7, type: "text/plain;charset=utf-8"}
6.一次性流
因为Body混入是构建在ReadableStream之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调
用一次,再次调用就会抛出错误。
即使是在读取流的过程中,所有这些方法也会在它们被调用时给ReadableStream加锁,以阻止其他读取器访问。
作为Body混入的一部分,bodyUsed布尔值属性表示ReadableStream是否已摄受(disturbed),意思是读取器是否已经在流上加了锁。这不一定表示流已经被完全读取。
下面的代码演示了这个属性:
let request = new Request('https://foo.com',{method: 'POST', body: 'foobar'});let response = new Response('foobar');console.log(request.bodyUsed); // falseconsole.log(response.bodyUsed); // falserequest.text().then(console.log); // foobarresponse.text().then(console.log); // foobarconsole.log(request.bodyUsed); // trueconsole.log(response.bodyUsed); // true
7.使用ReadableStream主体
JavaScript编程逻辑很多时候会将访问网络作为原子操作,比如请求是同时创建和发送的,响应数据也是以统一的格式一次性暴露出来的。
这种约定隐藏了底层的混乱,让涉及网络的代码变得很清晰。
从TCP/IP角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。
接收端点会为此分配内存,并将收到的块写入内存。
Fetch API通过ReadableStream支持在这些块到达时就实时读取和操作这些数据。
正如Stream API所定义的,ReadableStream暴露了getReader()方法,用于产生ReadableStream-DefaultReader,这个读取器可以用于在数据到达时异步获取数据块。
数据流的格式是Uint8Array。
