记忆犹新,在 2015 年 JavaScript 引入了 Fetch API,引起了开发者广泛的热情。Fetch 这种管理本地和远程资源的新方法很快被浏览器所采用,但 Node.js 却花了更长的时间。直到 2022 年才将 fetch() 添加到 Node 的标准库中。
虽然 Fetch API 很快成为在 Node 应用程序中发出 HTTP 请求的选择,但它的实现仍然落后于当前的标准。因为,Fetch API 还是存在一些限制和缺点,阻碍了其潜力的充分发挥。而核心团队可能需要几年的时间才能解决所有这些问题。
但是,功夫不负有心人。为了解决这些问题,ultrafetch 出现了,而它的目标就是解决 Node 中 Fetch API 实现的问题,从而改善开发人员的体验。
Fetch 与 Node.js
为了使 Fetch API 完全符合 HTTP RFC 标准,它还需要进行大量的工作。可能需要一两年左右的时间,我们才能看到 Fetch API 在 Node 中完全稳定下来。
当前 Fetch 实现的一个主要缺陷是缺乏内置的、符合标准的缓存系统。缓存对于提高性能和减少对同一端点的冗余请求至关重要,特别是在处理频繁请求的数据时。
在撰写本文时,缓存获取响应的唯一方法是使用自定义逻辑或外部缓存库将它们存储在内存或磁盘上。但这么做增加了代码的复杂性,并可能导致代码实现不一致的问题。
幸运的是,ultrafetch 可以解决这个问题。
ultrafetch
ultrafetch 是一个 Node.js 库,它提供了模块化的脚本,用于增强标准的 fetch 和 node-fetch 库。此外,它还可以扩展任何遵循 Bring Your Own Fetch (BYOF)方法的库,例如 @vercel/Fetch
。
ultrafetch 的主要目标是增强 Fetch API,使其与 rfc-7234 兼容的缓存系统相匹配。该库使用内存中的 Map
实例作为默认缓存引擎,用于存储由 Fetch API
的 GET、POST
或 PATCH
请求生成的响应对象。此外,ultrafetch 还支持自定义缓存引擎,以满足特定的需求。
为了理解这个库的工作原理,我用一个示例简单给大家说明一下:
假设作为 API 端点的业务逻辑的一部分,你的 Node.js 后端需要发出一个 HTTP GET 请求来获取一些数据。每次对该端点的 API 调用都需要一个新的 HTTP GET 请求。
如果这个请求总是返回相同的数据,你可以第一次缓存响应,然后在接下来的时间里从内存中读取它。 而 ultrafetch 就是这样做的。
虽然 ultrafetch 确实有助于增强获取行为,但它也存在一些需要解决的缺点。首先,尽管它被称为 “Ultra Fetch”,但它只增加了一个重要的 Fetch 功能。
接下来,我将提供一些示例来演示 ultrafetch。
如何使用 ultrafetch 添加缓存
安装 ultrafetch
和 node-fetch
使用以下命令将 ultrafetch 添加到项目的依赖项中:
代码语言:javascript复制npm install ultrafetch
作为一个 TypeScript 库,ultrafetch 已经自带了类型。所以,你不需要添加任何@types
库。
此外,如前所述,fetch 已经是 Node.js 18 的一部分,因此不需要安装它。如果你更喜欢使用 node-fetch
,使用如下命令安装:
npm install node-fetch
使用 ultrafetch 扩展 fetch
你所要做的就是用 withCache()
函数来扩展一个带有 ultrafetch
的 fetch
实现:
// import fetch from "node-fetch" -> if you are a node-fetch user
import { withCache } from "ultrafetch"
// extend the default fetch implementation
const enhancedFetch = withCache(fetch)
withCache()
通过添加缓存功能来增强读取功能。
然后,就像使用 fetch()
一样,你现在就可以使用增强版的 fetch()
函数:
const response = await enhancedFetch("https://example/api/xxx")
console.log (response.json ())
ultrafetch 的工作方式是不是与标准的 Fetch 非常相似。然而,当进行重复请求时,你就会看到增强版 Fetch 的能力了:
在第一次请求之后,响应对象将被添加到内部内存中的 Map
缓存中。当重复请求时,ultrafetch 将从缓存中提取并立即返回响应,而不需要进行网络通信,这大大节省了时间和资源。
你可以从 ultrafetch
导入并使用 isCached()
来验证这种行为:
import { withCache, isCached } from "ultrafetch"
当从缓存中返回给定的响应对象时,此函数返回 true。否则,返回 false。
在下面的例子中测试它:
代码语言:javascript复制const response1 = await enhancedFetch("https://example/api/xxx")
console.log(isCached(response1)) // false
const response2 = await enhancedFetch("https://example/api/xxx")
console.log(isCached(response1)) // true
第一个请求被执行,而第二个请求的响应按预期从缓存中读取。
添加自定义缓存
ultrafetch 引入的默认缓存功能很棒,但也有限制,因为它不允许访问和控制内部缓存。以及,无法通过编程方式清除缓存或从中删除特定项。一旦发出请求,它将永远被缓存,这可能不是期望的行为。出于这个原因,withCache
也接受一个自定义缓存对象:
class MyCustomCache extends Map<string, string> {
// 使用自定义逻辑覆盖
}
const fetchCache: MyCustomCache = new MyCustomCache()
const enhancedFetch = withCache(fetch, { cache: fetchCache })
这种机制使你可以控制缓存对象。例如,现在可以使用以下命令清除缓存:
代码语言:javascript复制fetchCache.clear()
不过,你需要注意的是,缓存参数必须为 Map<string, string>或AsyncMap<string, string>
类型。
如果要编写一些自定义缓存逻辑,你可以使用自定义的 ultrafetch
类型,如下面的 AsyncMap
:
export interface AsyncMap<K, V> {
clear(): Promise<void>;
delete(key: K): Promise<boolean>;
get(key: K): Promise<V | undefined>;
has(key: K): Promise<boolean>;
set(key: K, value: V): Promise<this>;
readonly size: number;
}
上一个完整一点的例子
现在,我打算在一个完整的例子中使用 ultrafetch,看看它的能力:
代码语言:javascript复制import { withCache, isCached } from "ultrafetch"
// 定义一个自定义缓存
class MyCustomCache extends Map<string, string> {
// 使用自定义逻辑覆盖
}
const fetchCache: MyCustomCache = new MyCustomCache()
// 扩展 fetch 的实现
const enhancedFetch = withCache(fetch, { cache: fetchCache })
async function testUltraFetch() {
console.time("request-1")
const response1 = await enhancedFetch("https://example/api/xxx")
console.timeEnd("request-1") // ~ 300ms
const isResponse1Cached = isCached(response1)
console.log(`request-1 cached: ${isResponse1Cached}n`) // false
console.time("request-2")
const response2 = await enhancedFetch("https://example/api/xxx")
console.timeEnd("request-2") // ~ 30ms
const isResponse2Cached = isCached(response2)
console.log(`request-2 cached: ${isResponse2Cached}n`) // true
// 清除缓存
fetchCache.clear()
console.time("request-3")
const response3 = await enhancedFetch("https://example/api/xxx")
console.timeEnd("request-3") // ~300ms (may be a lower because of server-side caching from PokéAPI)
const isResponse3Cached = isCached(response3)
console.log(`request-3 cached: ${isResponse3Cached}n`) // false
}
testUltraFetch()
现在,运行 Node 脚本,输出如下结果:
代码语言:javascript复制request-1: 234.871ms
request-1 cached: false
request-2: 21.473ms
request-2 cached: true
request-3: 109.328ms
request-3 cached: false
除了 isCached()
之外,你还可以通过查看请求时间来证明 ultrafetch
正在从缓存中读取 request-2
响应。清除缓存后,request-3
的行为应该和 request-1
一样。
除此之外, ultrafetch 不会仅基于目标 API 端点缓存响应。它还会考虑请求的报头和正文。
具体来说,它对 fetch 的 header 和 body 对象进行散列,并将结果字符串连接到 <HTTP_METHOD>:<request_url>
中。
结尾
最后,总结一下。ultrafetch 它为 fetch 和 node-fetch 模块增加了缓存功能,解决了 Fetch API 没有提供标准化的方式来缓存服务器响应。
使用 ultrafetch,你可以轻松地缓存由任何符合 fetch 的实现生成的 HTTP 响应,从而节省时间和资源,避免在不必要的请求上浪费。