此 Codelab 是 Google Developers 培训团队开发的“开发渐进式 Web 应用”培训课程的一部分。如果您按顺序学习这些 Codelab,将会充分发掘此课程的价值。
如需详细了解本课程,请参阅开发渐进式 Web 应用概览。
简介
本实验将引导您使用 Fetch API(一种用于提取资源的简单接口),该接口是对 XMLHttpRequest API 的改进。
学习内容
- 如何使用 Fetch API 请求资源
- 如何使用 fetch 发出 GET、HEAD 和 POST 请求
- 如何读取和设置自定义标头
- CORS 的使用和限制
注意事项
- JavaScript 和 HTML 基础知识
- 熟悉 ES2015 Promise 的概念和基本语法
所需条件
注意:虽然 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.json
和package.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.json
。fetch
会返回一个 promise,该 promise 会解析为 Response 对象。如果 promise 得到解决,则会将响应传递给 logResult
函数。如果 Promise 被拒绝,catch
会接管控制权,并将错误传递给 logError
函数。
响应对象表示对请求的响应。它们包含响应正文以及有用的属性和方法。
测试无效的回答
在控制台中检查已记录的响应。请记下 status
、url
和 ok
属性的值。
将 fetchJSON
中的 examples/animals.json
资源替换为 examples/non-existent.json
。更新后的 fetchJSON
函数现在应如下所示:
function fetchJSON() {
fetch('examples/non-existent.json')
.then(logResult)
.catch(logError);
}
保存脚本并刷新页面。再次点击 Fetch JSON,尝试提取此不存在的资源。
观察到提取已成功完成,并且未触发 catch
代码块。现在,找到新响应的 status
、URL
和 ok
属性。
这两个文件中的值应有所不同(您知道为什么吗?)。如果您收到任何控制台错误,这些值是否与错误上下文相符?
说明
为什么失败的响应未激活 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;
}
然后,添加一个将响应读取为 Blob 的 readResponseAsBlob
函数:
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
函数更新为
- 提取
/examples/words.txt
- 使用
validateResponse
验证回答 - 将响应读取为文本(提示:请参阅 Response.text())
- 并在网页上显示文本
您可以使用此 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
函数,以记录响应 headers
的 content-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 模式(如错误日志所示),并移除了对 validateResponse
和 readResponseAsText
的调用(请参阅下文说明):
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 访问响应(因此我们无法使用 validateResponse
、readResponseAsText
或 showResponse
)。响应仍可供其他 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-Type
为 application/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。