原生 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 点半更新,坚持阅读,自律打卡,每天一次,进步一点

相关推荐
bysking27 分钟前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓43 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4111 小时前
无网络安装ionic和运行
前端·npm
理想不理想v1 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205871 小时前
web端手机录音
前端
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
暮毅1 小时前
10.Node.js连接MongoDb
数据库·mongodb·node.js
神仙别闹1 小时前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
aPurpleBerry2 小时前
JS常用数组方法 reduce filter find forEach
javascript