up-fetch:基于 fetch API 的高级客户端构建器项目

Advanced fetch client builder

分支1Tags136
文件最后提交记录最后更新时间
3 个月前
1 年前
1 年前
3 个月前
3 个月前
3 个月前
5 个月前
1 年前
1 年前
1 年前
3 个月前
3 个月前
8 个月前
10 个月前
1 年前
8 个月前
3 个月前
3 个月前
8 个月前
8 个月前
1 年前

upfetch - 高级 fetch 客户端构建器


upfetch


npm version license commit activity downloads per month


upfetch 是一个高级的 fetch 客户端构建器,具有标准模式验证、自动响应解析、智能默认值等功能。旨在使数据获取类型安全且开发人员友好,同时保持熟悉的 fetch API。

目录

➡️ 亮点

  • 🚀 轻量级 - 压缩后仅 1.6kB,无依赖
  • 🔒 类型安全 - 使用 zodvalibotarktype 验证 API 响应
  • 🛠️ 实用的 API - 使用对象作为 paramsbody,自动获取解析后的响应
  • 🎨 灵活配置 - 一次设置 baseUrlheaders 等默认值,随处使用
  • 🎯 全面 - 内置重试、超时、进度跟踪、流式传输、生命周期钩子等功能
  • 🤝 熟悉的使用方式 - 与 fetch 相同的 API,但具有额外选项和合理的默认值

➡️ Agent Skill

使用以下命令安装 up-fetch skill:

npx skills add L-Blondy/up-fetch

➡️ 快速开始

npm i up-fetch

创建新的 upfetch 实例:

import { up } from 'up-fetch'

export const upfetch = up(fetch)

使用模式验证进行 fetch 请求:

import { upfetch } from './upfetch'
import { z } from 'zod'

const user = await upfetch('https://a.b.c/users/1', {
   schema: z.object({
      id: z.number(),
      name: z.string(),
      avatar: z.string().url(),
   }),
})

响应已经基于模式进行了解析和正确的类型推断

upfetch 扩展了原生 fetch API,这意味着所有标准的 fetch 选项都可用。

➡️ 主要特性

✔️ 请求配置

创建实例时为所有请求设置默认值:

const upfetch = up(fetch, () => ({
   baseUrl: 'https://a.b.c',
   timeout: 30000,
}))

查看 API 参考 获取完整的选项列表。

✔️ 简单查询参数

👎 使用原生 fetch:

fetch(
   `https://api.example.com/todos?search=${search}&skip=${skip}&take=${take}`,
)

👍 使用 upfetch

upfetch('/todos', {
   params: { search, skip, take },
})

使用 serializeParams 选项自定义查询参数序列化。

✔️ 自动处理请求体

👎 使用原生 fetch:

fetch('https://api.example.com/todos', {
   method: 'POST',
   headers: { 'Content-Type': 'application/json' },
   body: JSON.stringify({ title: 'New Todo' }),
})

👍 使用 upfetch

upfetch('/todos', {
   method: 'POST',
   body: { title: 'New Todo' },
})

upfetch 也支持所有 fetch body 类型

查看 serializeBody 选项自定义请求体序列化。

✔️ 模式验证

由于 upfetch 遵循 Standard Schema Specification,它可以与任何实现该规范的模式库一起使用。
查看完整列表 这里

👉 使用 zod 3.24+

import { z } from 'zod'

const posts = await upfetch('/posts/1', {
   schema: z.object({
      id: z.number(),
      title: z.string(),
   }),
})

👉 使用 valibot 1.0+

import { object, string, number } from 'valibot'

const posts = await upfetch('/posts/1', {
   schema: object({
      id: number(),
      title: string(),
   }),
})

✔️ 生命周期钩子

使用简单的钩子控制请求/响应生命周期:

const upfetch = up(fetch, () => ({
   onRequest: (options) => {
      // 在发出请求之前调用,可以在此处修改选项
   },
   onSuccess: (data, options) => {
      // 请求成功完成时调用
   },
   onError: (error, options) => {
      // 请求失败时调用
   },
}))

✔️ 超时设置

为单个请求设置超时:

upfetch('/todos', {
   timeout: 3000,
})

为所有请求设置默认超时:

const upfetch = up(fetch, () => ({
   timeout: 5000,
}))

✔️ 重试

重试功能允许您自动重试失败的请求,可配置尝试次数、延迟和条件。

const upfetch = up(fetch, () => ({
   retry: {
      attempts: 3,
      delay: 1000,
   },
}))

示例:

每个请求的重试配置
await upfetch('/api/data', {
   method: 'DELETE',
   retry: {
      attempts: 2,
   },
})
指数退避重试
const upfetch = up(fetch, () => ({
   retry: {
      attempts: 3,
      delay: (ctx) => ctx.attempt ** 2 * 1000,
   },
}))
基于请求方法的重试
const upfetch = up(fetch, () => ({
   retry: {
      // GET 请求重试一次,其他方法不重试:
      attempts: (ctx) => (ctx.request.method === 'GET' ? 1 : 0),
      delay: 1000,
   },
}))
基于响应状态码的重试
const upfetch = up(fetch, () => ({
   retry: {
      when({ response }) {
         if (!response) return false
         return [408, 413, 429, 500, 502, 503, 504].includes(response.status)
      },
      attempts: 1,
      delay: 1000,
   },
}))
网络错误、超时或其他错误时重试
const upfetch = up(fetch, () => ({
   retry: {
      attempts: 2,
      delay: 1000,
      when: (ctx) => {
         // 超时错误时重试
         if (ctx.error) return ctx.error.name === 'TimeoutError'
         // 429 服务器错误时重试
         if (ctx.response) return ctx.response.status === 429
         return false
      },
   },
}))

✔️ 错误处理

👉 ResponseError

response.okfalse 时抛出。
使用 isResponseError 识别此错误类型。

import { isResponseError } from 'up-fetch'

try {
   await upfetch('/todos/1')
} catch (error) {
   if (isResponseError(error)) {
      console.log(error.status)
   }
}
  • 使用 parseRejected 选项抛出自定义错误。
  • 使用 reject 选项决定何时抛出错误。

👉 ResponseValidationError

当模式验证失败时抛出。
使用 isResponseValidationError 识别此错误类型。

import { isResponseValidationError } from 'up-fetch'

try {
   await upfetch('/todos/1', { schema: todoSchema })
} catch (error) {
   if (isResponseValidationError(error)) {
      console.log(error.issues)
   }
}

➡️ 使用方法

✔️ 身份验证

你可以通过设置默认 header 轻松为所有请求添加身份验证。

在每次请求之前从 localStorage 获取 bearer token:

const upfetch = up(fetch, () => ({
   headers: { Authorization: localStorage.getItem('bearer-token') },
}))

获取异步 token:

const upfetch = up(fetch, async () => ({
   headers: { Authorization: await getToken() },
}))

✔️ 删除默认选项

只需传递 undefined

upfetch('/todos', {
   signal: undefined,
})

对于单独的 paramsheaders 也同样适用:

upfetch('/todos', {
   headers: { Authorization: undefined },
})

✔️ 表单数据

form 获取 FormData。

const form = document.querySelector('#my-form')

upfetch('/todos', {
   method: 'POST',
   body: new FormData(form),
})

或从对象创建 FormData:

import { serialize } from 'object-to-formdata'

const upfetch = up(fetch, () => ({
   serializeBody: (body) => serialize(body),
}))

upfetch('https://a.b.c', {
   method: 'POST',
   body: { file: new File(['foo'], 'foo.txt') },
})

✔️ 多个 fetch 客户端

你可以创建多个具有不同默认值的 upfetch 实例:

const fetchMovie = up(fetch, () => ({
   baseUrl: 'https://api.themoviedb.org',
   headers: {
      accept: 'application/json',
      Authorization: `Bearer ${process.env.API_KEY}`,
   },
}))

const fetchFile = up(fetch, () => ({
   parseResponse: async (res) => {
      const name = res.url.split('/').at(-1) ?? ''
      const type = res.headers.get('content-type') ?? ''
      return new File([await res.blob()], name, { type })
   },
}))

✔️ 流式传输

upfetch 通过 onRequestStreaming 提供上传操作的强大流式传输功能,通过 onResponseStreaming 提供下载操作的流式传输功能。

这两个处理器都接收以下属性:

  • chunk: Uint8Array:当前正在流式传输的数据块
  • transferredBytes: number:到目前为止已传输的数据量
  • totalBytes?: number:数据的总大小,从 "Content-Length" 头部读取。
    对于请求流式传输,如果头部不存在,总字节数将从请求体中读取。

以下是处理 AI 聊天机器人流式响应的示例:

const decoder = new TextDecoder()

upfetch('/ai-chatbot', {
   onResponseStreaming: ({ chunk }) => {
      const text = decoder.decode(chunk, { stream: true })
      console.log(text)
   },
})

✔️ 进度

👉 上传进度:

upfetch('/upload', {
   method: 'POST',
   body: new File(['large file'], 'foo.txt'),
   onRequestStreaming: ({ transferredBytes, totalBytes }) => {
      console.log(`进度:${transferredBytes} / ${totalBytes}`)
   },
})

👉 下载进度:

upfetch('/download', {
   onResponseStreaming: ({
      transferredBytes,
      totalBytes = transferredBytes,
   }) => {
      console.log(`进度:${transferredBytes} / ${totalBytes}`)
   },
})

➡️ 高级用法

✔️ 错误作为值

虽然 Fetch API 在响应不正常时不会抛出错误,但 upfetch 会抛出 ResponseError

如果你更愿意将错误作为值处理,将 reject 设置为返回 false
这允许你自定义 parseResponse 函数以结构化格式返回成功数据和错误响应。

const upfetch = up(fetch, () => ({
   reject: () => false,
   parseResponse: async (response) => {
      const json = await response.json()
      return response.ok
         ? { data: json, error: null }
         : { data: null, error: json }
   },
}))

使用方法:

const { data, error } = await upfetch('/users/1')

✔️ 自定义响应解析

默认情况下,upfetch 能够自动解析 jsontext 成功响应。

reject 返回 false 时调用 parseResponse 方法。 你可以使用该选项解析其他响应类型。

const upfetch = up(fetch, () => ({
   parseResponse: (response) => response.blob(),
}))

💡 注意,只有当 reject 返回 false 时才会调用 parseResponse 方法。

✔️ 自定义响应错误

默认情况下,当 reject 返回 true 时,upfetch 会抛出 ResponseError

如果你想抛出自定义错误或自定义错误消息,可以向 parseRejected 选项传递一个函数。

const upfetch = up(fetch, () => ({
   parseRejected: async (response) => {
      const data = await response.json()
      const status = response.status
      // 自定义错误消息
      const message = `Request failed with status ${status}: ${JSON.stringify(data)}`
      // 你也可以返回自定义错误类
      return new ResponseError({ message, status, data })
   },
}))

✔️ 自定义参数序列化

默认情况下,upfetch 使用 URLSearchParams 序列化参数。

你可以通过向 serializeParams 选项传递函数来自定义参数序列化。

import queryString from 'query-string'

const upfetch = up(fetch, () => ({
   serializeParams: (params) => queryString.stringify(params),
}))

✔️ 自定义请求体序列化

默认情况下,upfetch 使用 JSON.stringify 序列化普通对象。

你可以通过向 serializeBody 选项传递函数来自定义请求体序列化。它允许你:

  • 限制有效的请求体类型,通过类型化其第一个参数
  • 将请求体转换为有效的 BodyInit 类型

以下示例展示如何将有效请求体类型限制为 Record<string, any> 并使用 JSON.stringify 序列化:

// 将请求体类型限制为 Record<string, any> 并序列化
const upfetch = up(fetch, () => ({
   serializeBody: (body: Record<string, any>) => JSON.stringify(body),
}))

// ❌ 类型错误:请求体不是 Record<string, any>
upfetch('https://a.b.c/todos', {
   method: 'POST',
   body: [['title', 'New Todo']],
})

// ✅ 使用 Record<string, any> 正常工作
upfetch('https://a.b.c/todos', {
   method: 'POST',
   body: { title: 'New Todo' },
})

以下示例使用 superjson 序列化请求体。有效的请求体类型从 SuperJSON.stringify 推断。

import SuperJSON from 'superjson'

const upfetch = up(fetch, () => ({
   serializeBody: SuperJSON.stringify,
}))

✔️ 基于请求的默认值

默认选项接收 fetcher 参数,这允许你根据实际请求定制默认值。

const upfetch = up(fetch, (input, options) => ({
   baseUrl: 'https://example.com/',
   // 仅为受保护路由添加身份验证
   headers: {
      Authorization:
         typeof input === 'string' && input.startsWith('/api/protected/')
            ? `Bearer ${getToken()}`
            : undefined,
   },
   // 仅为公共端点添加跟踪参数
   params: {
      trackingId:
         typeof input === 'string' && input.startsWith('/public/')
            ? crypto.randomUUID()
            : undefined,
   },
   // 为长时间运行的操作增加超时时间
   timeout:
      typeof input === 'string' && input.startsWith('/export/') ? 30000 : 5000,
}))

➡️ API 参考

up(fetch, getDefaultOptions?)

创建具有可选默认选项的新 upfetch 实例。

function up(
   fetchFn: typeof globalThis.fetch,
   getDefaultOptions?: (
      input: RequestInit,
      options: FetcherOptions,
   ) => DefaultOptions | Promise<DefaultOptions>,
): UpFetch
选项 签名 描述
baseUrl string 所有请求的基础 URL。
onError (error, request) => void 发生错误时执行。
onSuccess (data, request) => void 请求成功完成时执行。
onRequest (request) => void 在发出请求之前执行。
onRetry (ctx) => void 在每次重试之前执行。
onRequestStreaming (event, request) => void 每次发送请求数据块时执行。
onResponseStreaming (event, response) => void 每次接收响应数据块时执行。
params object 默认查询参数。
parseResponse (response, request) => data 默认成功响应解析器。
如果省略,将自动解析 jsontext 响应。
parseRejected (response, request) => error 默认错误响应解析器。
如果省略,将自动解析 jsontext 响应。
reject (response) => boolean 决定何时拒绝响应。
retry RetryOptions 默认重试选项。
serializeBody (body) => BodyInit 默认请求体序列化器。
通过类型化其第一个参数限制有效的 body 类型。
serializeParams (params) => string 默认查询参数序列化器。
timeout number 默认超时时间(毫秒)。
...以及所有其他 fetch 选项

upfetch(url, options?)

使用给定选项发出 fetch 请求。

function upfetch(
   url: string | URL | Request,
   options?: FetcherOptions,
): Promise<any>

选项:

选项 签名 描述
baseUrl string 请求的基础 URL。
onError (error, request) => void 发生错误时执行。
onSuccess (data, request) => void 请求成功完成时执行。
onRequest (request) => void 在发出请求之前执行。
onRetry (ctx) => void 在每次重试之前执行。
onRequestStreaming (event, request) => void 每次发送请求数据块时执行。
onResponseStreaming (event, response) => void 每次接收响应数据块时执行。
params object 查询参数。
parseResponse (response, request) => data 成功响应解析器。
parseRejected (response, request) => error 错误响应解析器。
reject (response) => boolean 决定何时拒绝响应。
retry RetryOptions 重试选项。
schema StandardSchemaV1 用于验证响应的模式。
模式必须遵循 Standard Schema Specification
serializeBody (body) => BodyInit 请求体序列化器。
通过类型化其第一个参数限制有效的 body 类型。
serializeParams (params) => string 查询参数序列化器。
timeout number 超时时间(毫秒)。
...以及所有其他 fetch 选项

RetryOptions

选项 签名 描述
when (ctx) => boolean 基于响应或错误决定是否应该重试的函数
attempts number | function 重试次数或基于请求确定重试次数的函数
delay number | function 重试之间的延迟(毫秒)或基于尝试次数确定延迟的函数

isResponseError(error)

检查错误是否为 ResponseError

isResponseValidationError(error)

检查错误是否为 ResponseValidationError

isJsonifiable(value)

确定值是否可以安全转换为 json

以下被认为是可 JSON 化的:

  • 普通对象
  • 数组
  • 具有 toJSON 方法的类实例

➡️ 功能比较

查看功能比较表,了解 upfetch 与其他获取库的比较。


➡️ 环境支持

  • ✅ 浏览器 (Chrome, Firefox, Safari, Edge)
  • ✅ Node.js (18.0+)
  • ✅ Bun
  • ✅ Deno
  • ✅ Cloudflare Workers
  • ✅ Vercel Edge Runtime



分享到:

s Share on Twitter