大家好,这里是大家的林语冰。
本期共享的是 ------ 如何让原生 fetch
API 与 Node、Deno 和 Bun "梦幻联动",重点内容包括但不限于:
- 客户端 vs 服务端中
fetch
使用的异同点:虽然fetch
API 跨客户端和服务器环境提供了一致的接口,但我们重点介绍了使用限制方面的主要差异,比如客户端的 CORS 和 CSP,以及约束不高、但潜力极大的服务端第三方 API 限制。 - 现代 JS 环境中的
fetch
API:我们会探讨如何在 Node、Deno 和 Bun 等各种 JS 环境中跨运行时使用fetch
API,这是一种比XMLHttpRequest
更简单且现代化的替代方案,重点关注其Promise
筑基的结构和易用性。 - 有效的
fetch
请求策略:我们强调有效的fetch
请求策略的重要性,比如使用Promise.allSettled
进行并发请求,并使用AbortController
管理超时请求,从而确保 Web App 中的性能优化和更好的错误处理。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 How to use the Fetch API in Node.js, Deno, and Bun。
fetch
API 与 XMLHttpRequest
通过 HTTP 请求获取数据是基本的 Web App 活动。我们可能会在浏览器中进行了此类调用,但 Node、Deno 和 Bun 也原生支持 fetch
API。
在浏览器中,我们可能会向服务器请求信息,这样无需全屏刷新即可显示该信息。这通常被称为 Ajax 请求或 SPA(单页应用程序)。从 1999 年到 2015 年,XMLHttpRequest
是唯一的选择 ------ 如果我们想显示文件上传进度,那么它仍然是不二法门。XMLHttpRequest
是一个相当猪头、回调筑基的 API,但它允许细粒度的控制。尽管 Ajax 全名是"异步的 JS + XML",但它仍可以处理 XML 以外格式的响应,比如文本、二进制、JSON
和 HTML。
浏览器从 2015 年开始就实现了 fetch
API,它是一种比 XMLHttpRequest
更精简、更容易、更一致、Promise
筑基的替代方案。
我们的服务端代码可能还想发送 HTTP 请求 ------ 这通常是调用其他服务器上的 API。从第一个版本开始,Deno 和 Bun 运行时都有效地拷贝了浏览器的 fetch
API,这样类似的代码可以同时在客户端和服务器上运行。Node 则需要第三方模块,比如 node-fetch
或 axios
,直到 2022 年 2 月,Node 18 添加了标准 fetch
API,这仍然被认为是实验性的,但现在在大多数情况下,我们可以在任意位置的相同代码中使用 fetch()
。
fetch
基本示例
举个栗子,从 URI fetch
响应数据:
js
const response = await fetch('https://example.com/data.json')
fetch()
调用返回一个 Promise
,该 Promise
通过提供有关结果信息的 Response
对象进行解析。我们可以使用 Promise
筑基的 .json()
方法,将 HTTP 响应正文解析为 JS 对象:
js
const data = await response.json()
// 获取响应的 JSON 数据,使用数据搞事情
fetch
:客户端 vs 服务端
跨平台的 API 可能是相同的,但浏览器在发出客户端 fetch()
请求时会强制执行限制:
- CORS(跨域资源共享):客户端 JS 能且仅能与自己域内的 API 端点通信。从
domainA
(域名 A)加载的脚本可以调用相同域名的任何服务,比如domainA.com/api
。但不可能调用domainB
(域名 B)上的服务 ------ 除非该服务器通过设置 HTTPAccess-Control-Allow-Origin
header 来允许访问。 - CSP(内容安全策略):我们的网站/App 可以设置
Content-Security-Policy
HTTP header 或元标记,从而控制页面中允许的资源。CSP 可以防止意外或恶意脚本注入、iframe
、字体、图像、视频等。举个栗子,设置default-src 'self'
会停止fetch()
请求其自身域名之外的数据,XMLHttpRequest
、WebSocket
、服务器发送的事件和 beacon 也有所限制。
Node、Deno 和 Bun 中的服务端 fetch
API 调用限制较少,我们可以从任何服务器请求数据。换而言之,第三方 API 可能:
- 需要使用密钥或 OAuth 进行某种身份验证或授权
- 具有最大请求阈值,比如每分钟不超过一次调用
- 或者收取商业访问费用
我们可以使用服务端 fetch()
调用来代理客户端请求,这可以避免 CORS 和 CSP 问题。换而言之,粉丝请记住成为一个有责任心的网络公民,不要使用成百上千个请求轰炸服务器,这可能导致服务器瘫痪!
自定义 fetch
请求
上述例子的表面下,JS 创建一个 Request
对象,它表示该请求的完整详细信息,比如方法、header、正文等。
fetch()
接受两个参数:
- resource(资源) :字符串或
URL
对象 - 可选 options 选项参数:进一步设置请求
举个栗子:
js
const response = await fetch('https://example.com/data.json', {
method: 'GET',
credentials: 'omit',
redirect: 'error',
priority: 'high'
})
options
对象可以在 Node 或客户端代码中设置以下属性:
属性 | 值 |
---|---|
method |
GET (默认)、POST/PUT/PATCH/DELETE/HEAD |
headers |
字符串或 Headers 对象 |
body |
可以是字符串、JSON 、blob 等 |
mode |
same-origin/no-cors/cors |
credentials |
omit/same-origin/include cookie 和 HTTP 身份验证 header |
redirect |
follow/error/manual 重定向处理 |
referrer |
引用 URL |
integrity |
子资源完整性哈希URL |
signal |
用于取消请求的 AbortSignal 对象 |
或者,我们可以创建一个 Request
对象,并将其传递给 fetch()
。如果我们可以提前定义 API 端点,或想要发送一系列类似的请求,这可能很实用:
js
const request = new Request('https://example.com/api/', {
method: 'POST',
body: '{"a": 1, "b": 2, "c": 3}',
credentials: 'omit'
})
console.log(`fetching ${request.url}`)
const response = await fetch(request)
处理 HTTP Headers
我们可以使用 Headers
对象操作和检查请求和响应中的 HTTP header。如果您使用过 JS Map 对象,那么该 API 会似曾相识:
js
// 设置初始 headers
const headers = new Headers({
'Content-Type': 'text/plain'
})
// 添加 header
headers.append('Authorization', 'Basic abc123')
// 添加/更改 header
headers.set('Content-Type', 'application/json')
// 获取 header
const type = headers.get('Content-Type')
// 判断 header?
if (headers.has('Authorization')) {
// 删除 header
headers.delete('Authorization')
}
// 循环所有 headers
headers.forEach((value, name) => {
console.log(`${name}: ${value}`)
})
// 在 fetch() 中使用
const response = await fetch('https://example.com/data.json', {
method: 'GET',
headers
})
// response.headers 返回一个 Headers 对象
response.headers.forEach((value, name) => {
console.log(`${name}: ${value}`)
})
fetch Promise
的 resolve 和 reject
您可能认为当端点返回 404 Not Found 或类似的服务器错误时, fetch() Promise
会被 reject。事实并非如此!该 Promise
还是会 resolve,因为该调用是成功的 ------ 即使 404 的结果并非如你所愿。
fetch() Promise
只在以下情况下 reject:
- 我们发送了无效请求 - 比如
fetch('httttps://!invalid\URL/');
- 我们中止了
fetch()
请求 - 出现网络错误,比如连接失败
分析 fetch
响应
成功的 fetch()
调用返回一个 Response
对象,其中包含有关状态和返回数据的信息。属性是:
属性 | 说明 |
---|---|
ok |
若响应成功则为 true |
status |
HTTP 状态码,比如 200 表示成功 |
statusText |
HTTP 状态文本,比如 200 状态码的 OK |
url |
URL 网址 |
redirected |
若请求被重定向则为 true |
type |
响应类型:basic/cors/error/opaque/opaqueredirect |
headers |
响应 Headers 对象 |
body |
body 内容的 ReadableStream 或 null |
bodyUsed |
若 body 已被读取则为 true |
以下 Response
对象方法都返回一个 Promise
,因此您应该使用 await
或 .then
区块:
方法 | 说明 |
---|---|
text() |
以字符串形式返回 body |
json() |
将 body 解析为 JSON |
arrayBuffer() |
以 ArrayBuffer 形式返回 body |
blob() |
以 Blob 形式返回 body |
formData() |
将 body 作为键/值对的 FormData 对象返回 |
clone() |
克隆响应,通常这样我们可以用不同的方式解析 body |
js
// response 示例
const response = await fetch('https://example.com/data.json')
// 响应返回 JSON?
if (
response.ok &&
response.headers.get('Content-Type') === 'application/json'
) {
// 解析 JSON
const obj = await response.json()
}
中止 fetch
请求
Node 不会使 fetch()
请求超时;它可以永远运行!浏览器也可以等待一到五分钟。正常情况下,如果我们期望得到光速响应,我们应该中止 fetch()
。
举个栗子,使用 AbortController
对象,它将 signal
属性传递给第二个 fetch()
参数。如果 fetch
没有在五秒内完事,那么 timeout(超时)就会运行 .abort()
方法:
js
// 创建 AbortController,五秒后超时中止
const controller = new AbortController(),
signal = controller.signal,
timeout = setTimeout(() => controller.abort(), 5000)
try {
const response = await fetch('https://example.com/slowrequest/', { signal })
clearTimeout(timeout)
console.log(response.ok)
} catch (err) {
// timeout or network error
console.log(err)
}
Node、Deno、Bun 和 2022 年中以来发布的大多数浏览器也支持 AbortSignal
。这提供了一个更简单的 timeout()
方法,因此我们不必管理自己的计时器:
js
try {
// 五秒后超时
const response = await fetch('https://example.com/slowrequest/', {
signal: AbortSignal.timeout(5000)
})
console.log(response.ok)
} catch (err) {
// 超时或网络错误
console.log(err)
}
高效 fetch
与任何异步、Promise
筑基的操作一样,当且仅当调用的输入依赖前一个调用的输出时,才应该连续进行 fetch()
调用。下述代码的执行效果不佳,因为每个 API 调用都必须等待前一个调用 resolve 或 reject。如果每个响应需要一秒钟,那么总共需要三秒钟才能完成:
js
// 低效
const response1 = await fetch('https://example1.com/api/')
const response2 = await fetch('https://example2.com/api/')
const response3 = await fetch('https://example3.com/api/')
Promise.allSettled()
方法并发运行 Promise
,并在所有 Promise
已经 resolve 或 reject 时 fulfill。该代码的速度由最慢的响应决定。速度会快三倍:
js
const data = await Promise.allSettled(
[
'https://example1.com/api/',
'https://example2.com/api/',
'https://example3.com/api/'
].map(url => fetch(url))
)
data
返回一个对象数组,其中:
- 每个都有
status
属性都是"fullfilled"
或"rejected"
字符串 - 如果已经 resolve,
value
属性会返回fetch()
响应 - 如果已经 reject,
reason
属性会返回错误
写在最后
除非我们使用 Node 17 或更低的旧版本,否则 fetch
API 在服务器和客户端上都支持。fetch()
灵活易用,并且在所有运行时都保持一致。当且仅当我们需要更高级的功能,比如缓存、重试或文件处理时,我们才需要第三方模块。
本期话题是 ------ 你平时开发更多使用原生 fetch
还是 axios 等第三方模块?
欢迎在本文下方自由言论,文明共享。谢谢大家的点赞,掰掰~
《前端猫猫教》每日 9 点半更新,坚持阅读,自律打卡,每天一次,进步一点。