五、Fetch API

  • Fetch API则必须是异步

1、基本用法

  • fetch()方法是暴露在全局作用域中的,包括主页面执行线程、模块和工作线程。

1)分派请求

  • fetch()只有一个必需的参数input。多数情况下,这个参数是要获取资源的URL。
  • 这个方法返回一个期约
1
2
let r = fetch('/bar'); 
console.log(r); // Promise <pending>
  • 请求完成、资源可用时,期约会解决为一个Response对象
  • 这个对象是API的封装,可以通过它取得相应资源。获取资源要使用这个对象的属性和方法,掌握响应的情况并将负载转换为有用的形式。
1
2
3
4
5
fetch('bar.txt') 
.then((response) => {
console.log(response);
});
// Response { type: "basic", url: ... }

2、读取响应

  • 读取响应内容的最简单方式是取得纯文本格式的内容,这要用到text()方法。
    • 这个方法返回一个期约,会解决为取得资源的完整内容。
1
2
3
4
5
6
7
fetch('bar.txt') 
.then((response) => {
response.text().then((data) => {
console.log(data);
});
});
// bar.txt 的内容
  • 内容的结构通常是打平的:
    1
    2
    3
    4
    fetch('bar.txt') 
    .then((response) => response.text())
    .then((data) => console.log(data));
    // bar.txt 的内容

3)处理状态码和请求失败

  • Fetch API支持通过Response的status ( 状态码)和statusText (状态文本)属性检查响应状态。
  • 成功获取响应的请求通常会产生值为200的状态码
  • 请求不存在的资源通常会产生值为 404 的状态码
  • 请求的 URL 如果抛出服务器错误会产生值为 500 的状态码
  • 只要服务器返回了响应,fetch()期约都会解决。
1
2
3
4
5
fetch('/bar') 
.then((response) => {
console.log(response.status); // 200
console.log(response.statusText); // OK
});
  • 通常状态码为200时就会被认为成功了,其他情况可以被认为未成功。为区分这两种情况,可以在状态码非200-299时检查Response对象的ok属性。
1
2
3
4
5
6
7
8
9
10
fetch('/bar') 
.then((response) => {
console.log(response.status); // 200
console.log(response.ok); // true
});
fetch('/does-not-exist')
.then((response) => {
console.log(response.status); // 404
console.log(response.ok); // false
});
  • 违反CORS、无网络连接、HTTPS错配及其他浏览器/网络策略问题都会导致期约被拒绝
  • 可以通过url属性检查通过fetch()发送请求时使用的完整URL。
1
2
3
4
5
6
7
8
9
10
// foo.com/bar/baz 发送的请求
console.log(window.location.href); // https://foo.com/bar/baz
fetch('qux').then((response) => console.log(response.url));
// https://foo.com/bar/qux
fetch('/qux').then((response) => console.log(response.url));
// https://foo.com/qux
fetch('//qux.com').then((response) => console.log(response.url));
// https://qux.com
fetch('https://qux.com').then((response) => console.log(response.url));
// https://qux.com

4)自定义选项

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

2、常见Fetch请求模式

1)发送JSON数据

1
2
3
4
5
6
7
8
9
10
11
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)在请求体中发送参数

1
2
3
4
5
6
7
8
9
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()也可以序列化并发送文件字段中的文件。
1
2
3
4
5
6
7
let imageFormData = new FormData(); 
let imageInput = document.querySelector("input[type='file']");
imageFormData.append('image', imageInput.files[0]);
fetch('/img-upload', {
method: 'POST',
body: imageFormData
});

4)加载Blob文件

  • Fetch API也能提供Blob类型的响应,而Blob又可以兼容多种浏览器API。
  • 可以使用响应对象上暴露的blob()方法。
    • 方法返回一个期约,解决为一个Blob的实例。可以将这个实例传给URL.create0bjectUrl()以生成可以添加给图片元素src属性的值。
1
2
3
4
5
6
const imageElement = document.querySelector('img'); 
fetch('my-image.png')
.then((response) => response.blob())
.then((blob) => {
imageElement.src = URL.createObjectURL(blob);
});

5)发送跨源请求

  • 从不同的源请求资源,响应要包含CORS头部才能保证浏览器收到响应。没有这些头部,跨源请求会失败并抛出错误。
1
2
3
fetch('//cross-origin.com'); 
// TypeError: Failed to fetch
// No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • 如果代码不需要访问响应,也可以发送no-cors请求。此时响应的type属性值为opaque,因此无法读取相应内容。这种方式适合发送探测请求或者将响应缓存起来供以后使用。
1
2
3
fetch('//cross-origin.com', { method: 'no-cors' }) 
.then((response) => console.log(response.type));
// opaque

6)中断请求

  • Fetch API支持通过AbortController/AbortSignal 对中断请求。
  • 调用AbortController.abort()会中断所有网络传输,特别适合希望停止传输大型负载的情况。
  • 中断进行中的fetch()请求会导致包含错误的拒绝。
1
2
3
4
5
6
let abortController = new AbortController(); 
fetch('wikipedia.zip', { signal: abortController.signal })
.catch(() => console.log('aborted!');
// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10);
// 已经中断

3、Headers对象

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

1)Headers与Map的相似之处

  • 因为HTTP头部本质上是序列化后的键/值对,它们的JavaScript表示则是中间接口
  • Headers与Map类型都有get()、set()、has()和delete()等实例方法。
  • Headers和Map都可以使用一个可迭代对象来初始化。
  • 都有相同的keys()、values()和entries()迭代器接口。

2)Headers独有的特性

1、 在初始化Headers对象时,可以使用键/值对形式的对象,而Map不可以。
2、 Headers对象通过append()方法支持添加多个值。在Headers实例中还不存在的头部上调用append()方法相当于调用set ()。

1
2
3
4
5
6
7
8
9
10
11
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
//==================================
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"

3)头部护卫

  • Headers对象使用护卫来防止不被允许的修改。
  • 不同的护卫设置会改变set()、append()和 delete()的行为。违反护卫限制会抛出TypeError。

4、Request对象

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

1)创建Request对象

  • 通过构造函数初始化Request对象,为此需要传入一个input参数,一般是URL。
1
2
3
let r = new Request('https://foo.com'); 
console.log(r);
// Request {...}
  • Request构造函数也接收第二个参数一个 init 对象。
  • 没有在init对象中涉及的值则会使用默认值。

2)克隆Request对象

  • Fetch API提供了两种不太一样的方式用于创建Request对象的副本:
    • 使用Request构造函数;
    • 使用clone()方法。
  • 将Request实例作为input参数传给Request构造函数,会得到该请求的一个副本:
1
2
3
let r1 = new Request('https://foo.com'); 
let r2 = new Request(r1);
console.log(r2.url); // https://foo.com/
  • 如果再传入init对象,则init对象的值会覆盖源对象中同名的值:
1
2
3
4
let r1 = new Request('https://foo.com'); 
let r2 = new Request(r1, {method: 'POST'});
console.log(r1.method); // GET
console.log(r2.method); // POST
  • 这种克隆方式并不总能得到一模一样的副本。最明显的是,第一个请求的请求体会被标记为“已使用”:
1
2
3
4
let r1 = new Request('https://foo.com', { method: 'POST', body: 'foobar' }); 
let r2 = new Request(r1);
console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false
  • 第二种克隆Request对象的方式是使用clone()方法,这个方法会创建一模一样的副本,任何值都不会被覆盖。
  • 与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”。
  • 如果请求对象的bodyUsed属性为true (即请求体已被读取),那么上述任何一种方式都不能用来创建这个对象的副本
1
2
3
4
5
6
let r1 = new Request('https://foo.com', { method: 'POST', body: 'foobar' }); 
let r2 = r1.clone();
console.log(r1.url); // https://foo.com/
console.log(r2.url); // https://foo.com/
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

3)在fetch()中使用Request对象

  • 在调用fetch()时,可以传入已经创建好的Request实例而不是URL。
  • fetch()会在内部克隆传入的Requeat对象。
  • fetch()也不能拿请求体已经用过的Request对象来发送请求。
  • 有请求体的Request只能在一次fetch中使用
1
2
3
4
5
let r = new Request('https://foo.com'); 
// 向 foo.com 发送 GET 请求
fetch(r);
// 向 foo.com 发送 POST 请求
fetch(r, { method: 'POST' });
  • 要想基于包含请求体的相同Request对象多次调用fetch(),必须在第一次发送fetch()请求前调用clone()。
1
2
3
4
5
let r = new Request('https://foo.com', { method: 'POST', body: 'foobar' }); 
// 3 个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r);

5、Response对象

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

1)创建Response对象

  • 可以通过构造函数初始化Response对象且不需要参数。
  • 此时响应实例的属性均为默认值,因为它并不代表实际的HTTP响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
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对象,这个对象可以包含下表所列的键和值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch('https://foo.com') 
.then((response) => {
console.log(response);
});
/*Response {
body: (...)
bodyUsed: false
headers: Headers {}
ok: true
redirected: false
status: 200
statusText: "OK"
type: "basic"
url: "https://foo.com/"
}*/
  • Response类还有两个用于生成Response对象的静态方法: Response.redirect()和Response.error()。
    • 前者接收一个URL和一个重定向状态码(301、302、303、307或308),返回重定向的Response对象。
    • 提供的状态码必须对应重定向,否则会抛出错误。
  • 另一个静态方法Response.error()用于产生表示网络错误的Response 对象。
1
2
3
4
5
6
7
8
9
10
11
12
console.log(Response.redirect('https://foo.com', 301)); 
/*Response {
body: (...)
bodyUsed: false
headers: Headers {}
ok: false
redirected: false
status: 301
statusText: ""
type: "default"
url: ""
}*/

2)读取响应状态信息

  • Response对象包含一组只读属性,描述了请求完成后的状态,如下表所示。
  • 以下代码演示了返回200、302、404和500状态码的URL对应的响应:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fetch('//foo.com').then(console.log); 
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: "OK"
// type: "basic"
// url: "https://foo.com/"
// }
fetch('//foo.com/redirect-me').then(console.log);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: true
// status: 200
// statusText: "OK"
// type: "basic"
// url: "https://foo.com/redirected-url/"
// }
fetch('//foo.com/does-not-exist').then(console.log);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: true
// status: 404
// statusText: "Not Found"
// type: "basic"
// url: "https://foo.com/does-not-exist/"
// }
fetch('//foo.com/throws-error').then(console.log);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: true
// status: 500
// statusText: "Internal Server Error"
// type: "basic"
// url: "https://foo.com/throws-error/"
// }

3)克隆Response对象

  • 克隆Response对象的主要方式是使用clone()方法,这个方法会创建一个一模一样的副本,不会覆盖任何值。这样不会将任何请求的请求体标记为已使用。
1
2
3
4
let r1 = new Response('foobar'); 
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false
  • 如果响应对象的bodyUsed属性为true (即响应体已被读取),则不能再创建这个对象的副本。在响应体被读取之后再克隆会导致抛出TypeError。
1
2
3
4
5
6
let r = new Response('foobar'); 
r.clone();
// 没有错误
r.text(); // 设置 bodyUsed 为 true
r.clone();
// TypeError: Failed to execute 'clone' on 'Response': Response body is already used
  • 有响应体的Response对象只能读取一次。
1
2
3
4
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():
1
2
3
4
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

6、Request、Response及Body混入

1)Body.text()

  • Body.text ()方法返回期约,解决为将缓冲区转存得到的UTF-8 格式字符串。下面的代码展示了在Response对象上使用Body.text():
1
2
3
4
5
6
7
fetch('https://foo.com') 
.then((response) => response.text())
.then(console.log);
// <!doctype html><html lang="en">
// <head>
// <meta charset="utf-8">
// ...
  • 以下代码展示了在Request对象上使用Body.text():
1
2
3
4
let request = new Request('https://foo.com', { method: 'POST', body: 'barbazqux' }); 
request.text()
.then(console.log);
// barbazqux

2)Body.json()

  • Body.json()方法返回期约,解决为将缓冲区转存得到的JSON。下面的代码展示了在Response对象上使用Body.json():
1
2
3
4
fetch('https://foo.com/foo.json') 
.then((response) => response.json())
.then(console.log);
// {"foo": "bar"}
  • 以下代码展示了在Request对象上使用Body.json():
1
2
3
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对象序列化/反序列化为主体。
1
2
let myFormData = new FormData(); 
myFormData.append('foo', 'bar');
  • Body.formData()方法返回期约,解决为将缓冲区转存得到的FormData实例。
  • 下面的代码展示了在Response对象上使用Body.formData():
1
2
3
4
fetch('https://foo.com/form-data') 
.then((response) => response.formData())
.then((formData) => console.log(formData.get('foo'));
// bar
  • 以下代码展示了在Request对象上使用Body.formData():
1
2
3
4
5
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实例。
  • 下面的代码展示了在Response对象上使用Body.arrayBuffer():
1
2
3
4
fetch('https://foo.com') 
.then((response) => response.arrayBuffer())
.then(console.log);
// ArrayBuffer(...) {}
  • 以下代码展示了在Request对象上使用Body.arrayBuffer():
1
2
3
4
5
let request = new Request('https://foo.com', { method:'POST', body: 'abcdefg' }); 
// 以整数形式打印二进制编码的字符串
request.arrayBuffer()
.then((buf) => console.log(new Int8Array(buf)));
// Int8Array(7) [97, 98, 99, 100, 101, 102, 103]

5)Body.blob()

  • 有时候,可能需要以原始二进制格式使用主体,不用查看和修改。
  • 可以使用Body.blob()将主体内容转换为Blob实例。
  • Body.blob()方法返回期约,解决为将缓冲区转存得到的Blob实例
  • 下面的代码展示了在Response对象上使用Body.blob():
1
2
3
4
fetch('https://foo.com') 
.then((response) => response.blob())
.then(console.log);
// Blob(...) {size:..., type: "..."}
  • 以下代码展示了在Request对象上使用Body.blob():
1
2
3
let request = new Request('https://foo.com', { method:'POST', body: 'abcdefg' }); 
request.blob().then(console.log);
// Blob(7) {size: 7, type: "text/plain;charset=utf-8"}

6)一次性流

  • 因为Body混入是构建在ReadableStream之上的,所以主体流只能使用一次
  • 这意味着所有主体混入方法都只能调用一次,再次调用就会抛出错误。
1
2
3
4
5
6
fetch('https://foo.com') 
.then((response) => response.blob().then(() => response.blob()));
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked
let request = new Request('https://foo.com', { method: 'POST', body: 'foobar' });
request.blob().then(() => request.blob());
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked
  • 即使是在读取流的过程中,所有这些方法也会在它们被调用时给ReadableStream加锁,以阻止其他读取器访问:
1
2
3
4
5
6
7
8
9
10
fetch('https://foo.com') 
.then((response) => {
response.blob(); // 第一次调用给流加锁
response.blob(); // 第二次调用再次加锁会失败
});
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked
let request = new Request('https://foo.com', { method: 'POST', body: 'foobar' });
request.blob(); // 第一次调用给流加锁
request.blob(); // 第二次调用再次加锁会失败
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked