原文地址:https://javascript.info/fetch

文章 banner.png

JS 中有两种发起网络请求的方式,用来实现页面的局部刷新。分别是 XMLHttpRequestFetch API

XMLHttpRequest 俗称“AJAX”,你或许已经听说过了,它是 Asynchronous JavaScript And XML 的首字母简写形式,不过这个名字很具有误导性,XMLHttpRequest 几乎能够处理包括 XML 在内的几乎所有数据格式,只不过因为历史原因,这种命名被一直保留着,没做修改。

Fetch API 是最近引入的一个 API,以 fetch() 方法的形式暴露,它使用更加现代也更加强大,因此我们从它开始讲起。

fetch() 方法

基本语法如下:

  1. let promise = fetch(url, [options])
  • url —— 请求地址
  • options —— 配置参数。例如,设置请求方法、请求头等

如果不使用 options,默认发起 GET 请求。

得到的响应通常分两步处理。

fetch 函数返回的是一个 Promise 对象,使用内置的 Response 类实例作为 resolve 值,而且在服务器接收到响应头时就立即返回。

在这一阶段,我们可以检查 HTTP 状态码、查看请求是否成功,还可以检查响应头,不过还没有响应体数据。

需要注意的是,fetch 方法只有在无法触发 HTTP 请求(比如,网络中断了),或者请求地址不存在时,才会 reject,这时能用 catch 捕获错误。对返回的非正常状态码,类似 400 或 500 码,不会生成错误。

我们可以通过以下两个响应属性,查看 HTTP 状态:

  • status —— HTTP 状态码,如 200
  • ok —— 一个布尔值,当 HTTP 状态码介于 200~299 之间(包括 200 和 299)时,返回 true,否则返回 false。

举个例子:

  1. let response = await fetch(url);
  2. if (response.ok) { // if HTTP-status is 200-299
  3. // get the response body (the method explained below)
  4. let json = await response.json();
  5. } else {
  6. alert("HTTP-Error: " + response.status);
  7. }

第二阶段,获得响应体,这一步我们需要额外的方法调用。

Response 上提供了很多基于 Promise 的方法实现,能够让我们以不同的形式获取响应体数据:

  • response.text() —— 以文本的形式返回响应数据
  • response.json() —— 以 JSON 对象的形式返回响应数据
  • response.formData() —— 以 FormData 对象的形式返回响应数据
  • response.blob() —— 以 Blob(二进制数据) 对象的形式返回响应数据
  • response.arrayBuffer() —— 以 ArrayBuffer(二进制数据的底层表示) 对象的形式返回响应数据
  • 另外,还能通过 response.body 属性获得响应数据,这是一个 ReadableStream 对象,允许分块读取(chunk-by-chunk)响应数据,之后会看到例子。

举个例子,从 Github 上获得最新的提交信息,以 JSON 对象的形式:

  1. let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
  2. let response = await fetch(url);
  3. let commits = await response.json(); // read response body and parse as JSON
  4. alert(commits[0].author.login);

或者不使用 await,使用纯 Promise 语法来写:

  1. fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  2. .then(response => response.json())
  3. .then(commits => alert(commits[0].author.login));

如果需要以文本形式取得数据,就不是使用 .json() 了,而要用 await response.text():

  1. let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
  2. let text = await response.text(); // read response body as text
  3. alert(text.slice(0, 80) + '...');

下面再以 Fetch 规范 中出现的 logo 文件为例,展示如何读取二进制文件(查看 https://javascript.info/blob 一章,可以看到更加关于操作 Blob 对象的内容):

  1. let response = await fetch('/article/fetch/logo-fetch.svg');
  2. let blob = await response.blob(); // download as Blob object
  3. // create <img> for it
  4. let img = document.createElement('img');
  5. img.style = 'position:fixed;top:10px;left:10px;width:100px';
  6. document.body.append(img);
  7. // show it
  8. img.src = URL.createObjectURL(blob);
  9. setTimeout(() => { // hide after three seconds
  10. img.remove();
  11. URL.revokeObjectURL(img.src);
  12. }, 3000);

⚠️重要提示:对同一个 Response 对象,只能调用一次响应体读取(body-reading)方法

比如,你已经调用了 response.text(),再调用 response.json() 就不行了,因为响应体数据已经被处理过了。

  1. let text = await response.text(); // response body consumed
  2. let parsed = await response.json(); // fails (already consumed)

响应头

响应头数据可以通过 response.headers 获得。

response.headers 是一个类 Map 对象,提供了单独操作某个字段方法,还是一个可迭代对象:

  1. let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
  2. // get one header
  3. alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
  4. // iterate over all headers
  5. for (let [key, value] of response.headers) {
  6. alert(`${key} = ${value}`);
  7. }

下面展示了 response 的数据结构信息:

image.png

请求头

发送请求时,可以使用 headers 属性,配置请求头信息:

  1. let response = fetch(protectedUrl, {
  2. headers: {
  3. Authentication: 'secret'
  4. }
  5. });

以下是我们 不能通过此种方式设置的 HTTP 头部字段列表

  • Accept-Charset, Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie, Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

这是为了安全考虑,把这些字段的设置权限交给服务端,客户端不能控制。

POST 请求

如果发起的是 GET 请求之外的其他请求方法,就要使用 fetch 的配置对象了:

  • method —— 请求方法,比如 POST
  • body —— 请求体,以下类型之一
    • 字符串(比如,经过 JSON.stringify 方法编码后的)
    • FormData 对象,以 form/multipart 的形式发送数据
    • Blob/BufferSource,发送二进制数据
    • URLSearchParams,以 x-www-form-urlencoded 的形式发送数据,很少使用

多数时间,都是使用经过 JSON.stringify 方法编码后的字符串做请求体:

下例中,user 对象经 JSON 编码后跟随请求一起发送:

  1. let user = {
  2. name: 'John',
  3. surname: 'Smith'
  4. };
  5. let response = await fetch('/article/fetch/post/user', {
  6. method: 'POST',
  7. headers: {
  8. 'Content-Type': 'application/json;charset=utf-8'
  9. },
  10. body: JSON.stringify(user)
  11. });
  12. let result = await response.json();
  13. alert(result.message);

注意,这里的请求体 body 是个字符串,默认的 Content-Type 字段值会是 text/plain;charset=UTF-8。

因为,我们传输的是 JSON 数据,所以头部 headers 里要把 Content-Type 的值设置成 application/json,这才是正确的表达传输数据是经 JSON 编码后的。

发送图片

还能使用 Blob 或 BufferSource 对象发送二进制数据。

下例中,鼠标在 上移动能够绘制出线条,点击“submit”按钮后,像服务器发送刚才我们绘制的图片:

  1. <body style="margin:0">
  2. <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
  3. <input type="button" value="Submit" onclick="submit()">
  4. <script>
  5. canvasElem.onmousemove = function(e) {
  6. let ctx = canvasElem.getContext('2d');
  7. ctx.lineTo(e.clientX, e.clientY);
  8. ctx.stroke();
  9. };
  10. async function submit() {
  11. let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
  12. let response = await fetch('/article/fetch/post/image', {
  13. method: 'POST',
  14. body: blob
  15. });
  16. // the server responds with confirmation and the image size
  17. let result = await response.json();
  18. alert(result.message);
  19. }
  20. </script>
  21. </body>

注意,这里没有手动设置 Content-Type 头部信息,这是因为 Blob 有内置的类型信息(这里是 image/png,由 toBlob 方法生成)。在发送 Blob 对象时,内置类型会作为 Content-Type 的值使用。

当然,上述的方法还能使用纯 Promise 方法(不使用 async/await)改写成下面这样:

  1. function submit() {
  2. canvasElem.toBlob(function(blob) {
  3. fetch('/article/fetch/post/image', {
  4. method: 'POST',
  5. body: blob
  6. })
  7. .then(response => response.json())
  8. .then(result => alert(JSON.stringify(result, null, 2)))
  9. }, 'image/png');
  10. }

总结

fetch 请求通常分两步处理。await 形式:

  1. let response = await fetch(url, options); // resolves with response headers
  2. let result = await response.json(); // read body as json

纯 Promise 方法形式:

  1. fetch(url, options)
  2. .then(response => response.json())
  3. .then(result => /* process result */)

响应属性:

  • response.status – HTTP 状态码
  • response.ok – HTTP 状态码介于 200-299 之间时,返回 true,否则返回 false
  • response.headers – 以类 Map 对象形式返回的 HTTP 响应头信息

获得响应体的方法:

  • response.text() —— 以文本的形式返回响应数据
  • response.json() —— 以 JSON 对象的形式返回响应数据
  • response.formData() —— 以 FormData 对象的形式返回响应数据
  • response.blob() —— 以 Blob(二进制数据) 对象的形式返回响应数据
  • response.arrayBuffer() —— 以 ArrayBuffer(二进制数据的底层表示) 对象的形式返回响应数据

到目前为止,学到的选项字段:

  • method —— 请求方法
  • headers —— 以对象形式表达的请求头(有部分字段不支持设置)
  • body —— 发送的请求体数据,可以取 string、FormData、BufferSource、Blob 或 UrlSearchParams。

下章中,我们会看到更多的 fetch 选项以及它的使用用例。

(完)