2022年4月14日

获取

JavaScript 可以向服务器发送网络请求,并在需要时加载新信息。

例如,我们可以使用网络请求来

  • 提交订单,
  • 加载用户信息,
  • 接收来自服务器的最新更新,
  • …等等。

…而且所有这些都不需要重新加载页面!

从 JavaScript 发起网络请求有一个总称“AJAX”(缩写为Asynchronous JavaScript And XML)。虽然我们不必使用 XML:这个术语来自旧时代,所以这个词还在那里。你可能已经听说过这个术语了。

有多种方法可以发送网络请求并从服务器获取信息。

fetch() 方法是现代且通用的,所以我们将从它开始。它不受旧浏览器的支持(可以进行 polyfill),但在现代浏览器中得到了很好的支持。

基本语法是

let promise = fetch(url, [options])
  • url – 要访问的 URL。
  • options – 可选参数:方法、标题等。

如果没有 options,这是一个简单的 GET 请求,下载 url 的内容。

浏览器会立即启动请求并返回一个 promise,调用代码应该使用它来获取结果。

获取响应通常是一个两阶段过程。

首先,fetch 返回的 promise 在服务器响应标题后立即解析为内置 Response 类的对象。

在这个阶段,我们可以检查 HTTP 状态,以查看它是否成功,检查标题,但还没有主体。

如果 fetch 无法进行 HTTP 请求,例如网络问题或没有这样的站点,则 promise 会被拒绝。异常的 HTTP 状态,例如 404 或 500 不会导致错误。

我们可以在响应属性中看到 HTTP 状态

  • status – HTTP 状态码,例如 200。
  • ok – 布尔值,如果 HTTP 状态码为 200-299,则为 true

例如

let response = await fetch(url);

if (response.ok) { // if HTTP-status is 200-299
  // get the response body (the method explained below)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

其次,要获取响应主体,我们需要使用额外的函数调用。

Response 提供了多种基于 promise 的方法来以各种格式访问主体

  • response.text() – 读取响应并以文本形式返回,
  • response.json() – 将响应解析为 JSON,
  • response.formData() – 将响应作为 FormData 对象返回(在 下一章 中解释),
  • response.blob() – 将响应作为 Blob 返回(带有类型的二进制数据),
  • response.arrayBuffer() – 将响应作为 ArrayBuffer 返回(二进制数据的底层表示),
  • 此外,response.body 是一个 ReadableStream 对象,它允许你逐块读取主体,我们将在后面看到一个示例。

例如,让我们从 GitHub 获取一个包含最新提交的 JSON 对象

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // read response body and parse as JSON

alert(commits[0].author.login);

或者,使用纯 Promise 语法,不使用 `await` 来实现相同的功能。

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

要获取响应文本,使用 `await response.text()` 而不是 `response.json()`。

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

let text = await response.text(); // read response body as text

alert(text.slice(0, 80) + '...');

为了展示二进制格式的读取,让我们获取并显示 “fetch” 规范 的徽标图像(有关 `Blob` 操作的详细信息,请参见 Blob 章)。

let response = await fetch('/article/fetch/logo-fetch.svg');

let blob = await response.blob(); // download as Blob object

// create <img> for it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// show it
img.src = URL.createObjectURL(blob);

setTimeout(() => { // hide after three seconds
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);
重要

我们只能选择一种主体读取方法。

如果我们已经使用 `response.text()` 获取了响应,那么 `response.json()` 将不起作用,因为主体内容已经处理过了。

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

响应头

响应头在 `response.headers` 中的类似 Map 的 headers 对象中可用。

它不完全是 Map,但它具有类似的方法来按名称获取单个头或迭代它们。

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// get one header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// iterate over all headers
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

请求头

要在 `fetch` 中设置请求头,我们可以使用 `headers` 选项。它包含一个带有传出头的对象,如下所示。

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

…但是有一份 禁止的 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-*

这些头确保了正确的和安全的 HTTP,因此它们由浏览器独占控制。

POST 请求

要发出 `POST` 请求或使用其他方法的请求,我们需要使用 `fetch` 选项。

  • method – HTTP 方法,例如 `POST`,
  • body – 请求主体,可以是以下之一:
    • 字符串(例如 JSON 编码),
    • FormData 对象,用于以 `multipart/form-data` 形式提交数据,
    • Blob/BufferSource 用于发送二进制数据,
    • URLSearchParams,用于以 `x-www-form-urlencoded` 编码提交数据,很少使用。

JSON 格式在大多数情况下使用。

例如,这段代码将 `user` 对象作为 JSON 提交。

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

请注意,如果请求 `body` 是字符串,则默认情况下 `Content-Type` 头设置为 `text/plain;charset=UTF-8`。

但是,由于我们要发送 JSON,因此我们使用headers选项发送application/json,这是 JSON 编码数据的正确Content-Type

发送图像

我们还可以使用BlobBufferSource对象通过fetch提交二进制数据。

在此示例中,有一个<canvas>,我们可以在其中通过将鼠标悬停在上面来绘制。单击“提交”按钮会将图像发送到服务器

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Submit" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
        method: 'POST',
        body: blob
      });

      // the server responds with confirmation and the image size
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

请注意,这里我们没有手动设置Content-Type标头,因为Blob对象具有内置类型(此处为image/png,由toBlob生成)。对于Blob对象,该类型将成为Content-Type的值。

submit()函数可以像这样重写,无需使用async/await

function submit() {
  canvasElem.toBlob(function(blob) {
    fetch('/article/fetch/post/image', {
      method: 'POST',
      body: blob
    })
      .then(response => response.json())
      .then(result => alert(JSON.stringify(result, null, 2)))
  }, 'image/png');
}

总结

典型的 fetch 请求包含两个await调用

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

或者,不使用await

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

响应属性

  • response.status – 响应的 HTTP 代码,
  • response.ok – 如果状态为 200-299,则为true
  • response.headers – 包含 HTTP 标头的类似于 Map 的对象。

获取响应主体的方法

  • response.text() – 将响应作为文本返回,
  • response.json() – 将响应解析为 JSON 对象,
  • response.formData() – 将响应作为FormData对象返回(multipart/form-data编码,请参见下一章),
  • response.blob() – 将响应作为 Blob 返回(带有类型的二进制数据),
  • response.arrayBuffer() – 将响应作为ArrayBuffer(低级二进制数据)返回,

迄今为止的 Fetch 选项

  • method – HTTP 方法,
  • headers – 包含请求标头的对象(并非所有标头都允许),
  • body – 要发送的数据(请求主体)作为stringFormDataBufferSourceBlobUrlSearchParams对象。

在接下来的章节中,我们将看到更多fetch的选项和用例。

任务

创建一个异步函数getUsers(names),该函数获取一个 GitHub 登录名数组,从 GitHub 获取用户并返回一个 GitHub 用户数组。

对于给定的USERNAME,包含用户信息的 GitHub URL 为:https://api.github.com/users/USERNAME

沙箱中有一个测试示例。

重要细节

  1. 每个用户应该有一个fetch请求。
  2. 请求不应该互相等待。这样数据就可以尽快到达。
  3. 如果任何请求失败,或者没有这样的用户,该函数应该在结果数组中返回null

打开一个带有测试的沙箱。

要获取用户,我们需要:fetch('https://api.github.com/users/USERNAME')

如果响应的状态为200,则调用.json()读取 JS 对象。

否则,如果fetch失败,或者响应的状态不是 200,我们只需在结果数组中返回null

所以代码如下

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

请注意:.then 调用直接附加到 fetch,因此当我们获得响应时,它不会等待其他获取,而是立即开始读取 .json()

如果我们使用 await Promise.all(names.map(name => fetch(...))),并在结果上调用 .json(),那么它将等待所有获取响应。通过将 .json() 直接添加到每个 fetch,我们确保单个获取开始将数据读取为 JSON,而无需相互等待。

这是一个示例,说明即使我们主要使用 async/await,低级 Promise API 仍然有用。

在沙盒中打开带有测试的解决方案。

教程地图

评论

在评论之前阅读...
  • 如果您有任何改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请将它们包装在 <pre> 标签中,对于超过 10 行的代码,请使用沙盒(plnkrjsbincodepen…)