原生 fetch 跨运行时使用指南

大家好,这里是大家的林语冰。

本期共享的是 ------ 如何让原生 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-fetchaxios,直到 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)上的服务 ------ 除非该服务器通过设置 HTTP Access-Control-Allow-Origin header 来允许访问。
  • CSP(内容安全策略):我们的网站/App 可以设置 Content-Security-Policy HTTP header 或元标记,从而控制页面中允许的资源。CSP 可以防止意外或恶意脚本注入、iframe、字体、图像、视频等。举个栗子,设置 default-src 'self' 会停止 fetch() 请求其自身域名之外的数据,XMLHttpRequestWebSocket、服务器发送的事件和 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 内容的 ReadableStreamnull
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 点半更新,坚持阅读,自律打卡,每天一次,进步一点

相关推荐
菜根Sec3 分钟前
XSS跨站脚本攻击漏洞练习
前端·xss
m0_7482571810 分钟前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
桃园码工28 分钟前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲1 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
轻口味2 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami2 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda2 小时前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡2 小时前
lodash常用函数
前端·javascript
丰云2 小时前
一个简单封装的的nodejs缓存对象
缓存·node.js
emoji1111112 小时前
前端对页面数据进行缓存
开发语言·前端·javascript