Fetch API

此 Codelab 是 Google Developers 培训团队开发的“开发渐进式 Web 应用”培训课程的一部分。如果您按顺序学习这些 Codelab,将会充分发掘此课程的价值。

如需详细了解本课程,请参阅开发渐进式 Web 应用概览

简介

本实验将引导您使用 Fetch API(一种用于提取资源的简单接口),该接口是对 XMLHttpRequest API 的改进。

学习内容

  • 如何使用 Fetch API 请求资源
  • 如何使用 fetch 发出 GET、HEAD 和 POST 请求
  • 如何读取和设置自定义标头
  • CORS 的使用和限制

注意事项

  • JavaScript 和 HTML 基础知识
  • 熟悉 ES2015 Promise 的概念和基本语法

所需条件

  • 可访问终端/shell 的计算机
  • 连接到互联网
  • 支持 Fetch 的浏览器
  • 文本编辑器
  • Nodenpm

注意:虽然 Fetch API 目前并非在所有浏览器中都受支持,但存在 polyfill

从 GitHub 下载或克隆 pwa-training-labs 代码库,并根据需要安装 LTS 版本的 Node.js

打开计算机的命令行。进入 fetch-api-lab/app/ 目录并启动本地开发服务器:

cd fetch-api-lab/app
npm install
node server.js

您可以使用 Ctrl-c 随时终止服务器。

打开浏览器,然后前往 localhost:8081/。您应该会看到一个包含用于发出请求的按钮的页面(这些按钮目前还无法正常使用)。

注意:请取消注册所有 Service Worker 并清除 localhost 的所有 Service Worker 缓存,以免它们干扰实验。在 Chrome 开发者工具中,您可以通过点击应用标签页的清除存储空间部分中的清除网站数据来实现此目的。

在首选文本编辑器中打开 fetch-api-lab/app/ 文件夹。您将在 app/ 文件夹中构建实验。

此文件夹包含:

  • echo-servers/ 包含用于运行测试服务器的文件
  • examples/ 包含我们在实验性提取中使用的一些示例资源
  • js/main.js 是应用的主要 JavaScript,您将在其中编写所有代码
  • index.html 是我们示例网站/应用的主要 HTML 网页
  • package-lock.jsonpackage.json 是开发服务器和回显服务器依赖项的配置文件
  • server.js 是一个 Node 开发服务器

Fetch API 的接口相对简单。本部分介绍了如何使用 fetch 编写基本 HTTP 请求。

提取 JSON 文件

js/main.js 中,应用的 Fetch JSON 按钮附加到 fetchJSON 函数。

更新 fetchJSON 函数以请求 examples/animals.json 文件并记录响应:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(logResult)
    .catch(logError);
}

保存脚本并刷新页面。点击 Fetch JSON。控制台应记录提取响应。

说明

fetch 方法接受我们想要检索的资源的路径作为参数,在本例中为 examples/animals.jsonfetch 会返回一个 promise,该 promise 会解析为 Response 对象。如果 promise 得到解决,则会将响应传递给 logResult 函数。如果 Promise 被拒绝,catch 会接管控制权,并将错误传递给 logError 函数。

响应对象表示对请求的响应。它们包含响应正文以及有用的属性和方法。

测试无效的回答

在控制台中检查已记录的响应。请记下 statusurlok 属性的值。

fetchJSON 中的 examples/animals.json 资源替换为 examples/non-existent.json。更新后的 fetchJSON 函数现在应如下所示:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(logResult)
    .catch(logError);
}

保存脚本并刷新页面。再次点击 Fetch JSON,尝试提取此不存在的资源。

观察到提取已成功完成,并且未触发 catch 代码块。现在,找到新响应的 statusURLok 属性。

这两个文件中的值应有所不同(您知道为什么吗?)。如果您收到任何控制台错误,这些值是否与错误上下文相符?

说明

为什么失败的响应未激活 catch 代码块?这是关于提取和 Promise 的重要说明 - 错误响应(例如 404)仍会解析!只有在请求无法完成时,提取 promise 才会拒绝,因此您必须始终检查响应的有效性。我们将在下一部分中验证响应。

了解详情

检查回答的有效性

我们需要更新代码,以检查回答的有效性。

main.js 中,添加一个用于验证回答的函数:

function validateResponse(response) {
  if (!response.ok) {
    throw Error(response.statusText);
  }
  return response;
}

然后,将 fetchJSON 替换为以下代码:

function fetchJSON() {
  fetch('examples/non-existent.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

保存脚本并刷新页面。点击 Fetch JSON。检查控制台。现在,examples/non-existent.json 的响应应触发 catch 代码块。

fetchJSON 函数中的 examples/non-existent.json 替换为原始 examples/animals.json。更新后的函数现在应如下所示:

function fetchJSON() {
  fetch('examples/animals.json')
    .then(validateResponse)
    .then(logResult)
    .catch(logError);
}

保存脚本并刷新页面。点击 Fetch JSON。您应该会看到响应已成功记录,与之前一样。

说明

现在,我们添加了 validateResponse 检查,因此不良响应(例如 404)会抛出错误,并由 catch 接管。这样,我们就可以处理失败的响应,并防止意外的响应在提取链中向下传播。

阅读回答

提取响应表示为 ReadableStreams流规范),必须先读取响应,然后才能访问响应正文。响应对象具有用于执行此操作的方法

main.js 中,添加一个包含以下代码的 readResponseAsJSON 函数:

function readResponseAsJSON(response) {
  return response.json();
}

然后,将 fetchJSON 函数替换为以下代码:

function fetchJSON() {
  fetch('examples/animals.json') // 1
  .then(validateResponse) // 2
  .then(readResponseAsJSON) // 3
  .then(logResult) // 4
  .catch(logError);
}

保存脚本并刷新页面。点击 Fetch JSON。检查控制台,看看是否正在记录来自 examples/animals.json 的 JSON(而不是 Response 对象)。

说明

我们来回顾一下发生了什么。

第 1 步:对资源 examples/animals.json 调用了 fetch。Fetch 会返回一个 promise,该 promise 会解析为 Response 对象。当 promise 解决时,响应对象会传递给 validateResponse

第 2 步:validateResponse 检查响应是否有效(是否为 200?)。如果不是,则会抛出错误,跳过其余的 then 块并触发 catch 块。这一点尤为重要。如果不进行此检查,错误响应会传递到链中,可能会破坏依赖于接收有效响应的后续代码。如果响应有效,则会将其传递给 readResponseAsJSON

第 3 步:readResponseAsJSON 使用 Response.json() 方法读取响应正文。此方法会返回一个解析为 JSON 的 promise。此 Promise 解析后,JSON 数据会传递给 logResult。(如果 response.json() 中的 promise 被拒绝,则会触发 catch 块。)

第 4 步:最后,logResult 会记录来自对 examples/animals.json 的原始请求的 JSON 数据。

了解详情

提取的内容不限于 JSON。在此示例中,我们将提取图片并将其附加到网页。

main.js 中,使用以下代码编写 showImage 函数:

function showImage(responseAsBlob) {
  const container = document.getElementById('img-container');
  const imgElem = document.createElement('img');
  container.appendChild(imgElem);
  const imgUrl = URL.createObjectURL(responseAsBlob);
  imgElem.src = imgUrl;
}

然后,添加一个将响应读取为 BlobreadResponseAsBlob 函数:

function readResponseAsBlob(response) {
  return response.blob();
}

使用以下代码更新 fetchImage 函数:

function fetchImage() {
  fetch('examples/fetching.jpg')
    .then(validateResponse)
    .then(readResponseAsBlob)
    .then(showImage)
    .catch(logError);
}

保存脚本并刷新页面。点击提取图片。您应该会在页面上看到一只可爱的狗狗在捡拾一根棍子(这是个捡拾笑话!)。

说明

在此示例中,系统正在提取图片 examples/fetching.jpg。与上一个练习一样,系统会使用 validateResponse 验证响应。然后,将响应读取为 Blob(而不是像上一部分中那样读取为 JSON)。系统会创建一个图片元素并将其附加到网页,然后将图片的 src 属性设置为表示 Blob 的数据网址。

注意网址对象的 createObjectURL() 方法用于生成表示 Blob 的数据网址。请务必注意这一点。您无法将图片的来源直接设置为 Blob。必须将 Blob 转换为数据网址。

了解详情

此部分为可选挑战。

fetchText 函数更新为

  1. 提取 /examples/words.txt
  2. 使用 validateResponse 验证回答
  3. 将响应读取为文本(提示:请参阅 Response.text()
  4. 并在网页上显示文本

您可以使用此 showText 函数作为显示最终文本的辅助函数:

function showText(responseAsText) {
  const message = document.getElementById('message');
  message.textContent = responseAsText;
}

保存脚本并刷新页面。点击提取文本。如果您已正确实现 fetchText,则应该会在网页上看到添加的文字。

注意: 虽然使用 innerHTML 属性提取 HTML 并附加它可能很诱人,但请务必小心。这可能会导致您的网站遭到跨站脚本攻击

了解详情

默认情况下,fetch 使用 GET 方法,该方法用于检索特定资源。不过,fetch 也可以使用其他 HTTP 方法。

发出 HEAD 请求

headRequest 函数替换为以下代码:

function headRequest() {
  fetch('examples/words.txt', {
    method: 'HEAD'
  })
  .then(validateResponse)
  .then(readResponseAsText)
  .then(logResult)
  .catch(logError);
}

保存脚本并刷新页面。点击 HEAD 请求。观察到记录的文本内容为空。

说明

fetch 方法可以接收第二个可选参数 init。此参数可用于配置提取请求,例如请求方法、缓存模式、凭据

在此示例中,我们使用 init 参数将提取请求方法设置为 HEAD。HEAD 请求与 GET 请求类似,只是响应正文为空。如果您只需要文件的元数据,而不需要传输文件的所有数据,则可以使用此类请求。

可选:查找资源的大小

我们来看看 examples/words.txt 的提取响应的标头,以确定文件的大小。

更新 headRequest 函数,以记录响应 headerscontent-length 属性(提示:请参阅标头文档和 get 方法)。

更新代码后,保存文件并刷新页面。点击 HEAD 请求。控制台应记录 examples/words.txt 的大小(以字节为单位)。

说明

在此示例中,HEAD 方法用于请求资源(以 content-length 标头表示)的大小(以字节为单位),而无需实际加载资源本身。在实践中,这可用于确定是否应请求完整资源(甚至如何请求)。

可选:使用其他方法找出 examples/words.txt 的大小,并确认它与响应标头中的值一致(您可以查找如何针对您的特定操作系统执行此操作 - 如果使用命令行,则可获得奖励积分!)。

了解详情

Fetch 还可以通过 POST 请求发送数据。

设置回显服务器

在此示例中,您需要运行一个回显服务器。在 fetch-api-lab/app/ 目录中运行以下命令(如果命令行被 localhost:8081 服务器阻止,请打开新的命令行窗口或标签页):

node echo-servers/cors-server.js

此命令会在 localhost:5000/ 启动一个简单的服务器,该服务器会回显发送给它的请求。

您可以使用 ctrl+c 随时终止此服务器。

发出 POST 请求

postRequest 函数替换为以下代码(如果您未完成第 4 部分,请确保已定义 showText 函数):

function postRequest() {
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: 'name=david&message=hello'
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

保存脚本并刷新页面。点击发布请求。观察页面上回显的已发送请求。它应包含名称和消息(请注意,我们尚未从表单中获取数据)。

说明

如需使用 fetch 发出 POST 请求,我们使用 init 参数指定方法(类似于我们在上一部分中设置 HEAD 方法的方式)。我们还在此处设置了请求的正文,在本例中是一个简单的字符串。正文是我们想要发送的数据。

注意:在生产环境中,请务必对所有敏感用户数据进行加密。

当数据作为 POST 请求发送到 localhost:5000/ 时,该请求会作为响应回显。然后,系统会使用 validateResponse 验证响应,将其读取为文本,并显示在网页上。

在实践中,此服务器将代表第三方 API。

可选:使用 FormData 接口

您可以使用 FormData 接口轻松从表单中获取数据。

postRequest 函数中,从 msg-form 表单元素实例化一个新的 FormData 对象:

const formData = new FormData(document.getElementById('msg-form'));

然后,将 body 参数的值替换为 formData 变量。

保存脚本并刷新页面。填写页面上的表单(姓名消息字段),然后点击 POST 请求。观察页面上显示的表单内容。

说明

FormData 构造函数可以接收 HTML form,并创建一个 FormData 对象。此对象填充了表单的键和值。

了解详情

启动非 CORS 回显服务器

停止之前的回显服务器(通过在命令行中按 ctrl+c),然后通过运行以下命令从 fetch-lab-api/app/ 目录启动新的回显服务器:

node echo-servers/no-cors-server.js

此命令会设置另一个简单的回显服务器,这次是在 localhost:5001/。不过,此服务器未配置为接受跨源请求

从新服务器提取

现在,新服务器正在 localhost:5001/ 上运行,我们可以向其发送提取请求。

更新 postRequest 函数,使其从 localhost:5001/ 而不是 localhost:5000/ 中提取数据。更新代码后,保存文件,刷新页面,然后点击 POST 请求

您应该会在控制台中看到一条错误消息,指出由于缺少 CORS Access-Control-Allow-Origin 标头,跨源请求被阻止。

使用以下代码更新 postRequest 函数中的 fetch,该代码使用 no-cors 模式(如错误日志所示),并移除了对 validateResponsereadResponseAsText 的调用(请参阅下文说明):

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5001/', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
  })
    .then(logResult)
    .catch(logError);
}

保存脚本并刷新页面。然后,填写消息表单并点击 POST 请求

观察控制台中记录的响应对象。

说明

Fetch(和 XMLHttpRequest)遵循同源政策。这意味着浏览器会限制脚本中的非同源 HTTP 请求。当一个网域(例如 http://foo.com/)请求另一个网域(例如 http://bar.com/)中的资源时,就会发生跨源请求。

注意:跨源请求限制常常令人感到困惑。许多资源(例如图片、样式表和脚本)都是跨网域(即跨源)提取的。不过,这些都是同源政策的例外情况。跨源请求仍受脚本的限制。

由于我们应用的服务器具有不同于两个回显服务器的端口号,因此对任一回显服务器的请求都被视为跨源请求。不过,在 localhost:5000/ 上运行的第一个回显服务器已配置为支持 CORS(您可以打开 echo-servers/cors-server.js 并检查配置)。在 localhost:5001/ 上运行的新回显服务器不是(这就是我们收到错误的原因)。

使用 mode: no-cors 可提取不透明的响应。这样一来,我们便可以获得响应,但无法使用 JavaScript 访问响应(因此我们无法使用 validateResponsereadResponseAsTextshowResponse)。响应仍可供其他 API 使用或由服务工作线程缓存。

修改请求标头

Fetch 还支持修改请求标头。停止 localhost:5001(无 CORS)回显服务器,然后重新启动第 6 部分中的 localhost:5000 (CORS) 回显服务器:

node echo-servers/cors-server.js

恢复从 localhost:5000/ 中提取数据的 postRequest 函数的旧版本:

function postRequest() {
  const formData = new FormData(document.getElementById('msg-form'));
  fetch('http://localhost:5000/', {
    method: 'POST',
    body: formData
  })
    .then(validateResponse)
    .then(readResponseAsText)
    .then(showText)
    .catch(logError);
}

现在,使用 Header 接口postRequest 函数内创建一个名为 messageHeaders 的 Headers 对象,其中 Content-Type 标头等于 application/json

然后,将 init 对象的 headers 属性设置为 messageHeaders 变量。

body 属性更新为字符串化的 JSON 对象,例如:

JSON.stringify({ lab: 'fetch', status: 'fun' })

更新代码后,保存文件并刷新页面。然后点击 POST 请求

请注意,回显的请求现在的 Content-Typeapplication/json(而不是之前的 multipart/form-data)。

现在,向 messageHeaders 对象添加自定义 Content-Length 标头,并为请求指定任意大小。

更新代码后,保存文件,刷新网页,然后点击 POST 请求。请注意,此标头在回显的请求中未被修改。

说明

通过 Header 接口,可以创建和修改 Headers 对象。某些标头(例如 Content-Type)可以通过提取进行修改。其他属性(例如 Content-Length)是受保护的,无法修改(出于安全考虑)。

设置自定义请求标头

Fetch 支持设置自定义标头。

postRequest 函数中的 messageHeaders 对象中移除了 Content-Length 标头。添加具有任意值(例如“X-CUSTOM': 'hello world'”)的自定义标头 X-Custom

保存脚本,刷新页面,然后点击 POST 请求

您应该会看到,回显的请求中包含您添加的 X-Custom 属性。

现在,向 Headers 对象添加 Y-Custom 标头。保存脚本,刷新页面,然后点击 POST 请求

您应该会在控制台中看到类似如下的错误:

Fetch API cannot load http://localhost:5000/. Request header field y-custom is not allowed by Access-Control-Allow-Headers in preflight response.

说明

与非同源请求一样,自定义标头必须受到请求资源的服务器的支持。在此示例中,我们的回显服务器配置为接受 X-Custom 标头,但不接受 Y-Custom 标头(您可以打开 echo-servers/cors-server.js 并查找 Access-Control-Allow-Headers 来自行查看)。每次设置自定义标头时,浏览器都会执行预检检查。这意味着,浏览器会先向服务器发送 OPTIONS 请求,以确定服务器允许哪些 HTTP 方法和标头。如果服务器配置为接受原始请求的方法和标头,则会发送该请求,否则会抛出错误。

了解详情

解决方案代码

如需获取有效代码的副本,请前往 solution 文件夹。

您现在已经知道如何使用 Fetch API 了!

资源

如需查看 PWA 培训课程中的所有 Codelab,请参阅本课程的欢迎 Codelab