npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@soybeanjs/fetch

v0.1.0

Published

A lightweight, type-safe HTTP request library based on native fetch

Readme

@soybeanjs/fetch

English | 简体中文

一个基于原生 Fetch 封装的轻量级、类型安全的 HTTP 请求库,零运行时依赖,提供优雅的 API 设计和强大的功能支持。

✨ 特性

  • 🎯 类型安全:完整的 TypeScript 类型支持,智能类型推导
  • 🚀 零依赖:基于原生 fetch,无 axios 等运行时依赖
  • 🔄 双实例模式:支持标准请求实例和扁平化响应实例
  • 📦 文件下载:自动解析文件名和内容类型,支持多种文件格式
  • 🎣 生命周期钩子:提供完整的请求生命周期管理(传输层 + 业务层)
  • 🔁 自动重试:内置重试机制,支持自定义重试条件和延迟
  • ⏱️ 超时控制:基于 AbortController,可区分超时和用户取消
  • 🛡️ 错误处理:统一的错误处理机制,支持业务错误和网络错误
  • 📝 响应转换:灵活的响应数据转换功能
  • 🎨 状态管理:内置状态管理,可在实例间共享数据
  • 🔌 适配器 API:可插拔的传输层,支持 uniapp、微信小程序等平台
  • 🌐 $fetch API:兼容 ofetch 的轻量 fetch 客户端
  • 📡 传输层钩子:onRequest / onResponse / onRequestError / onResponseError,支持数组
  • 🔍 自动响应类型检测:根据 Content-Type 自动判断响应类型(含 SSE 支持)
  • 📤 上传进度:跨运行时上传进度跟踪,浏览器用 XHR,Node/Bun/Deno/CF 用 TransformStream
  • 📥 下载进度:基于 TransformStream 的下载进度跟踪
  • 💾 请求缓存:GET 响应缓存,支持 TTL、最大条数、自定义 key
  • 🔀 请求去重:自动合并相同在途请求,共享 Promise
  • 🚦 并发限制:限制同时在途请求数量,防止浏览器并发瓶颈
  • 📊 全局 Loading:自动追踪请求状态,支持慢请求告警

🤖 Agent Skills

本项目内置了一组 Agent Skills,为 AI 编程助手(Trae / Cursor / Windsurf 等)提供针对本库的开发指导。每个 skill 对应一类常见开发任务,包含关键文件定位、代码模式、硬约束与常见陷阱,确保 AI 生成的代码符合本库的两层架构与约定。

安装

在项目根目录执行以下命令即可安装全部 skills:

npx skills add soybeanjs/fetch

安装后 skills 会放在 skills/ 目录下,AI 助手会在匹配的任务场景中自动调用。

可用 Skills

| Skill | 覆盖任务 | 关键文件 | | ------------------------ | --------------------------------------------------------------------------------------------- | ----------------------------------- | | fetch-platform-adapter | 支持新平台(uniapp / 微信小程序 / React Native),自定义适配器,上传进度跟踪 | adapter.ts | | fetch-business-hook | 添加业务钩子(transform / isBackendSuccess / onBackendFail / onError / onRequest) | core.ts + fetch.ts | | fetch-transport-hook | 添加 ofetch 风格传输层钩子(onRequest / onResponse / onRequestError / onResponseError) | fetch.ts + types.ts | | fetch-enhanced-feature | 添加增强功能(cache / dedupe / concurrency / debounce / throttle / auth / schema / loading) | enhanced.ts | | fetch-retry-timeout | 配置重试次数 / 延迟 / 条件、超时控制、自定义重试状态码 | fetch.ts + options.ts | | fetch-openapi-typing | 扩展 OpenAPI 类型安全客户端(createTypedClient / toFlatTypedClient) | openapi.ts | | fetch-error-code | 添加自定义错误码、处理 FetchError / BackendError 错误模型 | error.ts + constant.ts | | fetch-test-writer | 按项目规范编写测试(vitest + 全局 fetch mock + 类型安全) | test/helpers.ts + test/setup.ts |

每个 skill 的详细内容见 skills/ 目录下的 SKILL.md

📦 安装

# npm
npm install @soybeanjs/fetch

# yarn
yarn add @soybeanjs/fetch

# pnpm
pnpm add @soybeanjs/fetch

环境要求:Node.js 18+ 或现代浏览器(需原生 fetch 支持)。

🚀 快速开始

基础使用

import { createRequest } from '@soybeanjs/fetch';
import type { FetchResponse } from '@soybeanjs/fetch';

interface ApiResponse<T = any> {
  code: number;
  data: T;
  message: string;
}

// 创建请求实例
const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    timeout: 10000
  },
  {
    // 转换响应数据
    // !!!注意这里一定要给 response 指定类型,这样才能有类型推导
    transform: (response: FetchResponse<ApiResponse>) => {
      return response.data.data;
    },
    // 请求前拦截
    onRequest: async config => {
      // 添加 token(headers 是原生 Headers 实例,使用 .set())
      config.headers.set('Authorization', `Bearer ${getToken()}`);
      return config;
    },
    // 判断后端业务是否成功
    isBackendSuccess: response => {
      return response.data.code === 200;
    },
    // 后端业务失败处理
    onBackendFail: async (response, instance) => {
      // 处理 token 过期等情况
      if (response.data.code === 401) {
        await refreshToken();
        // 重新发起请求(走完整管道)
        return instance(response.config);
      }
    },
    // 错误处理
    onError: async error => {
      console.error('Request failed:', error.message);
    }
  }
);

// 发起请求
const data = await request({
  url: '/users',
  method: 'GET'
});

扁平化响应实例 (toFlatRequest)

不抛出异常,通过返回值判断成功或失败。toFlatRequest 是一个包装器,它复用同一个 RequestInstancestate / instance / cache,不会重复创建请求流水线:

import { createRequest, toFlatRequest } from '@soybeanjs/fetch';

const request = createRequest({ baseURL: 'https://api.example.com' }, options);
const flatRequest = toFlatRequest(request);

const { data, error, response } = await flatRequest({
  url: '/users',
  method: 'GET'
});

if (error) {
  console.error('Request failed:', error);
} else {
  console.log('Success:', data);
}

$fetch — 轻量 fetch 客户端(兼容 ofetch)

无需业务逻辑,直接发起请求:

import { $fetch } from '@soybeanjs/fetch';

// GET 请求
const user = await $fetch<User>('/api/users/1');

// POST 请求
const created = await $fetch<User>('/api/users', {
  method: 'POST',
  body: { name: 'John' }
});

// 创建带默认值的实例
const apiFetch = $fetch.create({
  baseURL: 'https://api.example.com',
  headers: { Authorization: 'Bearer xxx' },
  retry: { retries: 3 }
});

// 获取完整响应(不抛异常)
const response = await $fetch.raw('/api/users/1');
console.log(response.status, response.data);

// 直接访问原生 fetch
$fetch.native('https://example.com');

📖 核心概念

RequestOption 配置项

| 配置项 | 类型 | 必填 | 说明 | | ------------------ | ---------- | ---- | -------------------------------------------------------------------------------------- | | transform | Function | 是 | 转换响应数据为业务数据 | | onRequest | Function | 否 | 请求前拦截器(业务层,返回值模式),可添加 token 等 | | isBackendSuccess | Function | 是 | 判断后端业务逻辑是否成功 | | onBackendFail | Function | 否 | 后端业务失败回调,如处理 token 过期。返回新 FetchResponse 可触发重试,新响应会再次校验 | | onError | Function | 否 | 请求错误处理,如显示错误提示 | | backendErrorMsg | string | 否 | 后端错误消息,用于构造 BackendError |

业务错误会以 BackendError 实例(继承自 FetchError,error.code === 'BACKEND_ERROR')形式抛出, 可通过 instanceof BackendErrorerror.code === BACKEND_ERROR_FLAG 判别,详见 错误判别

传输层钩子(对标 ofetch)

除了业务层钩子(RequestOption),FetchRequestConfig 还支持传输层钩子,支持单个函数或数组:

const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    // 传输层钩子(每个请求也可单独设置)
    onRequest: [
      ({ request, options }) => {
        console.log('→', request);
      }
    ],
    onResponse: [
      ({ response }) => {
        console.log('←', response.status);
      }
    ],
    onRequestError: [
      ({ error }) => {
        console.error('Request error:', error.message);
      }
    ],
    onResponseError: [
      ({ response, error }) => {
        console.error('Response error:', response.status);
      }
    ]
  },
  options
);

| 钩子 | 触发时机 | 参数 | | ----------------- | ----------------------- | ------------------------------ | | onRequest | 请求发送前 | FetchContext | | onRequestError | 请求失败(网络错误/超时) | FetchContext (含 error) | | onResponse | 响应接收并解析后 | FetchContext (含 response) | | onResponseError | 响应状态码错误(4xx/5xx) | FetchContext (含 error) |

与业务层钩子的区别: 传输层钩子在 FetchRequestConfig 中配置,支持数组和 FetchContext 模式;业务层钩子在 RequestOption 中配置,单个函数,返回值模式。传输层钩子先于业务层执行。

错误判别

业务错误(由 isBackendSuccess 判定为失败)会构造为 BackendError 实例:

import { FetchError, BackendError, BACKEND_ERROR_FLAG } from '@soybeanjs/fetch';

try {
  await request({ url: '/users/1' });
} catch (error) {
  if (error instanceof BackendError) {
    // 业务错误,例如 code !== 200
    console.error('业务错误:', error.message);
  } else if (error instanceof FetchError) {
    // 网络 / HTTP 错误
    console.error('网络错误:', error.message);
  }
}

// 或通过 code 判别(等价于 instanceof BackendError)
if (fetchError.code === BACKEND_ERROR_FLAG) {
  // ...
}

FetchError 提供以下便捷属性:

| 属性 | 说明 | | ------------ | ----------------------------------- | | status | HTTP 状态码(别名 statusCode) | | statusText | HTTP 状态文本(别名 statusMessage) | | data | 响应数据 | | code | 错误码 | | response | 完整 FetchResponse | | config | 请求配置 |

请求处理流程

用户发起请求
    ↓
传输层 onRequest 钩子(FetchContext 模式)
    ↓
业务层 onRequest 钩子(返回值模式)
    ↓
发送 HTTP 请求(adapter)
    ↓
├─ 网络错误 → 传输层 onRequestError → retry → onError
    ↓
接收响应 → 解析响应体(auto 自动检测类型)
    ↓
传输层 onResponse 钩子
    ↓
validateStatus 检查
    ├─ 错误状态码 → 传输层 onResponseError → retry → onError
    ↓
processResponse(业务逻辑)
    ├─ coerceBinaryToJsonResponse(二进制转 JSON)
    ├─ isBackendSuccess 校验
    │   ├─ 成功 → transform → 返回业务数据
    │   └─ 失败 → onBackendFail → BackendError → onError
    ├─ 文件类型 → 返回文件信息对象
    └─ 其他 → 返回原始数据

🎯 高级功能

1. 文件下载

支持自动解析文件名和内容类型:

// 下载文件
const fileData = await request({
  url: '/download/report.pdf',
  method: 'GET',
  responseType: 'blob'
});

// fileData 包含:
// {
//   file: Blob,
//   filename: 'report.pdf',
//   contentType: 'application/pdf'
// }

// 自定义文件名解析
const fileData = await request({
  url: '/download/file',
  responseType: 'blob',
  getFileName: response => {
    // 自定义解析逻辑
    return 'custom-filename.pdf';
  }
});

// 使用内置的 downloadFile 工具函数触发浏览器下载
import { downloadFile } from '@soybeanjs/fetch';

downloadFile(fileData.file, fileData.filename);

支持的文件类型:

  • blobFileResponseData<Blob>
  • arraybufferFileResponseData<ArrayBuffer>
  • streamFileResponseData<ReadableStream<Uint8Array>>

2. 响应类型支持

// JSON(默认),需要添加一个泛型参数指定业务数据类型,其他类型无需指定
interface UserData {
  id: number;
  name: string;
}
const data = await request<UserData>({
  url: '/users/123'
});

// auto — 根据 Content-Type 自动检测(推荐用于 $fetch)
const data = await $fetch('/api/data', { responseType: 'auto' });

// 文本
const text = await request({
  url: '/data.csv',
  responseType: 'text'
});

// HTML/XML 文档(使用 DOMParser)
const doc = await request({
  url: '/template.html',
  responseType: 'document'
});

// Blob(文件)
const file = await request({
  url: '/download/image.png',
  responseType: 'blob'
});

// ArrayBuffer
const buffer = await request({
  url: '/download/data.bin',
  responseType: 'arraybuffer'
});

// Stream(SSE / 大文件流)
const stream = await request({
  url: '/events',
  responseType: 'stream'
});

支持的响应类型:

| 类型 | 说明 | | ------------- | ------------------------------------------ | | json | JSON(默认) | | auto | 根据 Content-Type 自动检测 | | text | 文本 | | blob | Blob 文件 | | arraybuffer | ArrayBuffer | | stream | ReadableStream(含 SSE text/event-stream) | | document | HTML/XML 文档(DOMParser,浏览器环境) |

3. 状态管理

request.state 返回 EnhancedState,包含内置运行时状态(cache、loading、dedupe、messages 等),并支持通过索引签名直接扩展自定义字段:

const request = createRequest(
  { baseURL: 'https://api.example.com' },
  {
    // ...其他配置
    onRequest: config => {
      config.headers.set('Authorization', `Bearer ${request.state.token}`);
      return config;
    }
  }
);

// 用户自定义状态 —— 直接在 state 上读写
request.state.token = 'new-token';
request.state.userId = 123;

// 内置运行时状态也可访问
request.state.loading.count; // 当前并发请求数
request.state.cache.size; // 缓存条目数

消息去重

request.state.messages 是内置的 MessageStack 实例,用于请求消息去重。当请求在短时间内重复触发时,窗口内相同 key 的消息只通过首次:

const request = createRequest(
  { baseURL: 'https://api.example.com' },
  {
    isBackendSuccess: r => r.data.code === 200,
    onError: error => {
      // 3s 内同一 error.message 只展示一次
      if (request.state.messages.push(error.message)) {
        showToast(error.message);
      }
    }
  }
);

// 自定义去重 key(如按错误码去重)
if (request.state.messages.push(error.code, error.message)) {
  showToast(error.message);
}

// 调整时间窗口(默认 3000ms)
request.state.messages.interval = 5000;

// 查看窗口内活跃消息
request.state.messages.getActive();

// 清空消息栈
request.state.messages.clear();

4. 自动重试

内置重试机制,无需额外依赖:

const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    retry: {
      retries: 3,
      retryDelay: retryCount => retryCount * 1000,
      retryCondition: error => {
        // 仅在网络错误或 5xx 错误时重试
        return !error.response || error.response.status >= 500;
      }
    }
  },
  options
);

| 配置项 | 类型 | 默认值 | 说明 | | ---------------- | ---------------------------------------- | ------------------- | -------------- | | retries | number | 0 | 重试次数 | | retryDelay | (count, error) => number | 线性退避 | 重试延迟(毫秒) | | retryCondition | (error) => boolean \| Promise<boolean> | 网络错误+重试状态码 | 重试条件 |

默认重试状态码: 408, 409, 425, 429, 500, 502, 503, 504

用户主动取消(非超时)不会触发重试。

5. 超时控制

基于 AbortController 实现,可区分超时和用户取消:

const request = createRequest({ timeout: 10000 }, options);

// 超时会抛出 FetchError,code 为 'ERR_TIMEOUT'
// 错误消息格式: [GET] "https://...": Request timeout of 10000ms exceeded

// 用户主动取消(不会重试)
const controller = new AbortController();
const promise = request({
  url: '/users',
  signal: controller.signal
});

// 取消请求
controller.abort();

6. 类型推导

完整的 TypeScript 类型支持:

interface User {
  id: number;
  name: string;
}

interface ApiResponse<T = any> {
  code: number;
  data: T;
  message: string;
}

// ResponseData:后端原始响应类型
// ApiData:业务数据类型
const request = createRequest(
  { baseURL: 'https://api.example.com' },
  {
    transform: (response: FetchResponse<ApiResponse>) => response.data.data
  }
);

// 类型推导:data 的类型是 ApiResponse<User>
const user = await request<User>({
  url: '/users/123'
});

7. raw 方法 — 获取原始响应

通过 request.raw() 可以跳过 transform 转换,直接获取完整的 FetchResponse 对象。

与普通 request() 的区别:

| 方法 | 返回值 | 是否经过 transform | | --------------- | --------------------------- | ------------------ | | request() | 转换后的业务数据 | ✅ 是 | | request.raw() | 完整的 FetchResponse 对象 | ❌ 否 |

// 1. 获取响应头中的自定义信息
const response = await request.raw<User[]>({
  url: '/users',
  method: 'GET'
});

const totalCount = response.headers.get('x-total-count');
const requestId = response.headers.get('x-request-id');
const statusCode = response.status;

// 2. 文件下载时获取原始响应 + 文件信息
const fileResponse = await request.raw({
  url: '/download/report.pdf',
  responseType: 'blob'
});

// fileResponse.data 包含 { file, filename, contentType }

通过 toFlatRequest 包装得到的扁平化实例同样提供 flatRequest.raw() 方法,语义一致,但不抛异常。

8. 便捷 HTTP 方法

RequestInstance(由 createRequest 创建,并通过 withResponse 暴露)和通过 toFlatRequest 包装得到的 FlatRequestInstance 都提供了常用 HTTP 动词的快捷方法:

// GET 请求
const users = await request.get<User[]>('/users');
const usersWithQuery = await request.get<User[]>('/users', {
  query: { page: 1, pageSize: 10 }
});

// POST 请求
const newUser = await request.post<User>('/users', {
  name: '张三',
  email: '[email protected]'
});

// PUT 请求
const updatedUser = await request.put<User>('/users/123', {
  name: '张三(已更新)'
});

// PATCH 请求
const patchedUser = await request.patch<User>('/users/123', {
  email: '[email protected]'
});

// DELETE 请求
await request.delete('/users/123');

9. 适配器 API — 跨平台支持

通过自定义适配器,可在 uniapp、微信小程序等非标准 fetch 环境中运行:

import { createRequest, createAdapterResponse } from '@soybeanjs/fetch';

// uniapp 适配器示例
const uniappAdapter = async (url, init) => {
  const res = await uni.request({
    url,
    method: init.method as any,
    header: Object.fromEntries(init.headers.entries()),
    data: init.body,
    responseType: 'arraybuffer'
  });

  return createAdapterResponse({
    status: res.statusCode,
    statusText: '',
    headers: new Headers(res.header),
    body: res.data instanceof ArrayBuffer ? res.data : new ArrayBuffer(0)
  });
};

const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    adapter: uniappAdapter
  },
  options
);

10. ignoreResponseError — 忽略响应错误

ignoreResponseErrortrue 时,跳过 validateStatus 检查,返回响应而非抛出异常:

const response = await $fetch.raw('/api/users/404', {
  ignoreResponseError: true
});

// 即使是 404 也会返回 response,不会抛出 FetchError
console.log(response.status); // 404
console.log(response.data); // 错误页面数据

11. 类型安全客户端 (Typed Client / OpenAPI)

通过 openapi-typescript 生成 paths 类型后,可创建全类型安全的请求客户端。

OpenAPI 类型安全客户端通过独立的 @soybeanjs/fetch/openapi 子路径导入,按需加载、减小打包体积:

前置步骤:使用 openapi-typescriptopenapi.json 生成类型文件:

npx openapi-typescript ./openapi.json -o ./src/openapi.d.ts

createTypedClient

import { createRequest } from '@soybeanjs/fetch';
import { createTypedClient } from '@soybeanjs/fetch/openapi';
import type { paths } from './openapi.d.ts';

const request = createRequest({ baseURL: 'https://api.example.com' }, {/* ... */});

// Field = 'data' 用于解包 envelope 结构
const client = createTypedClient<paths, '/api/v1', 'data'>(request, '/api/v1');

// 路径、参数、请求体、返回值均有类型推导
const menus = await client.get('/menu/list', {
  query: { page: 1, pageSize: 10 }
});

// POST 请求
const loginResult = await client.post('/auth/login', {
  body: { username: 'admin', password: '123456' }
});

// 路径参数(替换 URL 中的 {id})
const user = await client.get('/users/{id}', {
  pathParams: { id: 1 }
});

// raw 方法 — 跳过 transform,返回完整 FetchResponse
const response = await client.raw.get('/menu/list', {
  query: { page: 1 }
});

toFlatTypedClient

包装一个 RequestInstance,生成不抛出异常的类型安全客户端。与 createTypedClient 使用同一个请求实例(共享 state/cache/auth):

import { createRequest, toFlatRequest } from '@soybeanjs/fetch';
import { createTypedClient, toFlatTypedClient } from '@soybeanjs/fetch/openapi';

const request = createRequest({ baseURL: 'https://api.example.com' }, {/* ... */});

const flatClient = toFlatTypedClient<paths, '/api/v1', 'data'>(request, '/api/v1');

const { data, error } = await flatClient.get('/menu/list', {
  query: { page: 1 }
});

if (error) {
  console.error('请求失败:', error.message);
} else {
  console.log('菜单:', data);
}

12. 上传进度跟踪

原生 fetch() API 不支持上传进度事件。本库通过 onUploadProgress 配置项弥补这一缺陷 —— 设置后库会自动为该请求切换到支持进度跟踪的适配器,无需手动管理适配器。

适用于 createRequest(包括经 toFlatRequest 包装得到的扁平化实例)、$fetch / createFetch 的所有 API,可在实例级或单次请求级使用。

跨运行时支持:库根据运行时自动选择最佳方案:

| 运行时 | 机制 | total 精度 | | -------------------- | -------------------------------- | ---------------------------------------------- | | 浏览器 | XMLHttpRequest.upload.progress | 精确 | | Node.js / Bun / Deno | TransformStream 字节计数 | 已知大小 body 精确(Blob/ArrayBuffer/string 等) | | CF Workers | TransformStream 字节计数 | 可能缓冲(进度瞬间跳 100%) |

对于 FormData 和原始 ReadableStream body,流式方案无法预知总大小,此时 lengthComputablefalse,但 loaded(已上传字节数)仍会更新。

基本用法(单次请求)

最常见场景:仅对上传接口设置进度回调。

import { createRequest } from '@soybeanjs/fetch';

const request = createRequest({ baseURL: 'https://api.example.com' }, {/* ... */});

async function uploadFile(file: File) {
  const formData = new FormData();
  formData.append('file', file);

  // 只需在请求配置中传入 onUploadProgress 回调
  const result = await request.post('/upload', formData, {
    onUploadProgress: ({ loaded, total, progress }) => {
      console.log(`上传进度: ${progress}% (${loaded}/${total} bytes)`);
    }
  });

  return result;
}

$fetch 同样支持:

import { $fetch } from '@soybeanjs/fetch';

await $fetch('/upload', {
  method: 'POST',
  body: formData,
  onUploadProgress: ({ progress }) => {
    progressBar.value = progress;
  }
});

进度事件

onUploadProgress 回调接收一个 UploadProgressEvent 对象:

| 属性 | 类型 | 说明 | | ------------------ | --------- | --------------------------------------------------------------------- | | loaded | number | 已上传的字节数 | | total | number | 总字节数(不可计算时为 0) | | progress | number | 上传进度百分比 0-100(不可计算时为 0) | | lengthComputable | boolean | 总大小是否已知。falsetotal/progress 为 0,但 loaded 仍有效 |

浏览器(XHR)模式下,仅当总大小已知时触发回调。流式模式下始终触发,通过 lengthComputable 区分。

在 Vue / React 中结合进度条

import { ref } from 'vue';
import { createRequest, toFlatRequest } from '@soybeanjs/fetch';

const uploadProgress = ref(0);

const request = toFlatRequest(
  createRequest({ baseURL: 'https://api.example.com' }, {/* ... */})
);

async function handleUpload(file: File) {
  const formData = new FormData();
  formData.append('file', file);

  const { data, error } = await request.post('/upload', formData, {
    onUploadProgress: ({ progress }) => {
      uploadProgress.value = progress;
    }
  });

  if (error) {
    console.error('上传失败:', error.message);
  } else {
    console.log('上传成功:', data);
  }

  uploadProgress.value = 0; // 重置
}

实例级配置(全局上传进度)

如需实例上所有请求都跟踪上传进度,可在创建实例时设置 onUploadProgress:

const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    onUploadProgress: ({ progress }) => {
      console.log(`Upload: ${progress}%`);
    }
  },
  options
);

高级:createUploadProgressAdapter

createUploadProgressAdapter 是底层构建函数,返回一个 FetchAdapter。跨运行时自动选择最佳方案(浏览器 XHR,其他环境 TransformStream)。适合需要将上传进度适配器与其他自定义适配器逻辑组合的场景:

import { createRequest, createUploadProgressAdapter } from '@soybeanjs/fetch';

const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    adapter: createUploadProgressAdapter(({ progress, loaded, lengthComputable }) => {
      if (lengthComputable) {
        console.log(`Upload: ${progress}%`);
      } else {
        console.log(`Uploaded ${loaded} bytes`);
      }
    })
  },
  options
);

当同时设置了 adapteronUploadProgress 时,adapter 优先,onUploadProgress 会被忽略。

13. 下载进度跟踪

通过 onDownloadProgress 配置项跟踪响应体下载进度。库会将响应体包装为计数的 TransformStream,每个 chunk 下载时触发回调。

适用于大文件下载场景,与上传进度对称。

// 单次请求
await request.get('/large-file', {
  responseType: 'blob',
  onDownloadProgress: ({ progress, loaded, total, lengthComputable }) => {
    if (lengthComputable) {
      console.log(`下载: ${progress}% (${loaded}/${total} bytes)`);
    } else {
      console.log(`已下载 ${loaded} bytes`);
    }
  }
});

// $fetch 也支持
const blob = await $fetch('/video.mp4', {
  responseType: 'blob',
  onDownloadProgress: ({ progress }) => {
    progressBar.value = progress;
  }
});

onDownloadProgress 回调接收 DownloadProgressEvent(与 UploadProgressEvent 结构相同):

| 属性 | 类型 | 说明 | | ------------------ | --------- | ------------------------------------------- | | loaded | number | 已下载字节数 | | total | number | 总字节数(从 Content-Length 获取,无则为 0) | | progress | number | 下载百分比 0-100(不可计算时为 0) | | lengthComputable | boolean | 总大小是否已知 |

14. 请求缓存

通过 cache 配置项缓存 GET 响应,避免重复请求。支持 TTL、最大条数、自定义 key 和按方法过滤。

const request = createRequest(
  {
    baseURL: '/api',
    cache: {
      ttl: 30000, // 缓存 30 秒
      methods: ['get'], // 仅缓存 GET(默认)
      max: 100 // 最多 100 条(默认),超出淘汰最旧
      // key: (config) => '...' // 自定义缓存 key
    }
  },
  options
);

// 第一次请求:发起网络调用
const a = await request.get('/user');
// 30 秒内的第二次请求:直接返回缓存
const b = await request.get('/user');

// 单次请求跳过缓存
const c = await request.get('/user', { cache: false });

缓存管理:实例上提供 clearCache()deleteCache(key) 方法手动管理缓存:

// 清除所有缓存
request.clearCache();

// 删除指定 key 的缓存(默认 key 格式为 "METHOD:url:query")
request.deleteCache('GET:/api/user:id=1');

// 更新数据后清除缓存,下次请求将重新拉取
await request.put('/user', newData);
request.clearCache();

15. 请求去重

通过 dedupe 配置项合并相同的在途请求。当多个相同的请求同时在途时,只发起一次网络调用,所有调用者共享同一个 Promise。

const request = createRequest(
  {
    baseURL: '/api',
    dedupe: true
  },
  options
);

// 两个并发相同请求 → 只发一次网络调用
const [a, b] = await Promise.all([request.get('/user'), request.get('/user')]);
console.log(a === b); // true(同一个响应)

// 自定义去重 key
const request2 = createRequest(
  {
    baseURL: '/api',
    dedupe: {
      key: config => `${config.method}:${config.url}`
    }
  },
  options
);

默认去重 key 为 method:url:query:body。请求完成后自动从去重表中移除。

16. 并发限制

通过 concurrency 配置项限制同时在途的请求数量。超出限制的请求排队等待,防止浏览器并发上限(6 个)导致的性能问题。

const request = createRequest({
  baseURL: '/api',
  concurrency: {
    maxConcurrent: 6   // 最多同时 6 个请求
  }
}, options);

// 批量上传 100 个文件,但最多同时上传 6 个
const files = [...]; // 100 files
const results = await Promise.all(
  files.map(file => request.post('/upload', file))
);

17. 全局 Loading 与慢请求追踪

通过 onGlobalLoadingChangeonLoadingChangeslowThresholdonSlowRequest 自动追踪请求状态。

const request = createRequest(
  {
    baseURL: '/api',
    // 全局 loading:第一个请求开始时 true,最后一个请求结束时 false
    onGlobalLoadingChange: loading => {
      store.globalLoading = loading;
    },
    // 慢请求:超过 10 秒触发告警
    slowThreshold: 10000,
    onSlowRequest: ({ url, method, duration }) => {
      console.warn(`慢请求: ${method} ${url} 已耗时 ${duration}ms`);
      // 上报监控...
    }
  },
  options
);

// 单请求 loading:该请求开始时 true,结束时 false
await request.post('/save', data, {
  onLoadingChange: loading => {
    saveBtnLoading.value = loading;
  }
});

| 配置项 | 类型 | 说明 | | ----------------------- | ----------------------------------- | --------------------------------------------------- | | onGlobalLoadingChange | (loading: boolean) => void | 全局 loading 状态变化回调(0→1 true,1→0 false) | | onLoadingChange | (loading: boolean) => void | 单请求 loading 状态回调(该请求开始 true,结束 false) | | slowThreshold | number | 慢请求阈值(ms),0 = 不启用(默认) | | onSlowRequest | (entry: SlowRequestEntry) => void | 慢请求回调,含 url/method/duration |

全局 vs 单请求 Loading:onGlobalLoadingChange 在第一个请求开始时触发 true,最后一个请求结束时触发 false,适合全局 loading 遮罩;onLoadingChange 对每个请求独立触发,适合按钮级别的 loading 状态。两者可同时使用。

18. 防抖与节流

通过 debouncethrottle 配置项控制请求频率。

防抖(debounce):延迟请求执行,延迟期间有新请求进入则取消前一个并重新计时。适用于搜索输入。

// 搜索输入防抖 300ms — 用户停止输入 300ms 后才发请求
searchInput.addEventListener('input', e => {
  request.get('/search', {
    query: { q: e.target.value },
    debounce: 300
  });
});

节流(throttle):固定间隔内只允许一次请求,额外请求被拒绝(ERR_THROTTLED)。适用于按钮防重复点击。

// 提交按钮节流 1 秒 — 1 秒内多次点击只发一次
submitButton.addEventListener('click', () => {
  request
    .post('/submit', formData, {
      throttle: 1000
    })
    .catch(err => {
      if (err.code === 'ERR_THROTTLED') return; // 忽略节流拒绝
      throw err;
    });
});

防抖和节流的 key 默认为 method:url:query:body,相同 key 的请求才会互相影响。

19. Auth 管理

通过 auth 配置项自动附加 Token 和处理 Token 刷新。

const request = createRequest(
  {
    baseURL: '/api',
    auth: {
      // 自动附加 Authorization: Bearer <token>
      getToken: () => localStorage.getItem('token'),

      // 刷新 token,返回新 token
      refreshToken: async () => {
        const res = await fetch('/auth/refresh', {
          headers: { 'refresh-token': localStorage.getItem('refreshToken') }
        });
        const { token } = await res.json();
        localStorage.setItem('token', token);
        return token;
      },

      // 何时触发刷新 —— 默认 401,可自定义状态码或判断函数
      // refreshOn: 403,                        // 403 时刷新
      refreshOn: (status, response) => {
        // 自定义判断
        return status === 401 || response.headers.get('x-token-expired') === '1';
      },

      // 刷新失败时调用(如跳转登录页)
      onUnauthorized: () => {
        router.push('/login');
      }
    }
  },
  options
);

| 属性 | 类型 | 说明 | | ---------------- | ----------------------------------------- | ---------------------------------- | | getToken | () => string \| Promise<string> | 获取当前 token,自动附加到请求头 | | refreshToken | () => Promise<string> | 刷新 token,返回新 token | | refreshOn | number \| (status, response) => boolean | 触发刷新的条件,默认 401 | | onUnauthorized | () => void | 刷新失败或无 refreshToken 时调用 |

并发刷新去重:多个请求同时触发刷新时,只调用一次 refreshToken,所有请求共享同一个刷新 Promise,刷新成功后全部自动重试。

20. 响应 Schema 验证

通过 schema 配置项在运行时验证响应数据结构,兼容 Standard Schema 规范(Zod v4+、Valibot、ArkType 等),也支持普通验证函数。

import { z } from 'zod'; // Zod v4+ 已实现 Standard Schema

// 使用 Standard Schema(Zod / Valibot / ArkType 等)
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email()
});

const user = await request.get('/user', { schema: UserSchema });
// user 已通过运行时验证,类型安全

// 使用普通验证函数(轻量 escape hatch)
const user2 = await request.get('/user', {
  schema: data => {
    if (!data.id) throw new Error('Missing id');
    return data;
  }
});

校验失败时抛出 FetchError(code: 'ERR_SCHEMA'),错误消息包含所有 issue 的路径与描述,可通过 onError 统一处理:

import { ERR_SCHEMA } from '@soybeanjs/fetch';

try {
  await request.get('/user', { schema: UserSchema });
} catch (error) {
  if (error.code === ERR_SCHEMA) {
    console.error('Schema 校验失败:', error.message);
  }
}

21. 请求/响应数据转换

通过 transformRequesttransformResponse 全局转换数据格式,如 camelCase ↔ snake_case。

import { camelCase, snakeCase } from 'lodash';

const request = createRequest(
  {
    baseURL: '/api',
    // 发送前:camelCase → snake_case
    transformRequest: body => {
      if (typeof body !== 'object' || body === null) return body;
      return Object.fromEntries(Object.entries(body).map(([k, v]) => [snakeCase(k), v]));
    },
    // 接收后:snake_case → camelCase
    transformResponse: data => {
      if (typeof data !== 'object' || data === null) return data;
      return Object.fromEntries(Object.entries(data).map(([k, v]) => [camelCase(k), v]));
    }
  },
  options
);

// 请求发送 { user_name: 'test' } (而非 { userName: 'test' })
// 响应自动转为 { userName: 'test' } (而非 { user_name: 'test' })
await request.post('/user', { userName: 'test' });

🛠️ 实用工具

parseContentDisposition

解析 Content-Disposition 响应头获取文件名:

import { parseContentDisposition } from '@soybeanjs/fetch';

const filename = parseContentDisposition("attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6.pdf");
// '文件.pdf'

downloadFile

在浏览器中触发文件下载:

import { downloadFile } from '@soybeanjs/fetch';

downloadFile(blob, 'report.pdf');

createFetch / $fetch

创建 ofetch 兼容的轻量 fetch 客户端:

import { $fetch, createFetch } from '@soybeanjs/fetch';

// 使用默认实例
const data = await $fetch('/api/users/1');

// 创建自定义实例
const apiFetch = createFetch({
  baseURL: 'https://api.example.com',
  retry: { retries: 3 },
  onRequest: [({ request }) => console.log('→', request)]
});

// 链式创建
const authFetch = apiFetch.create({
  headers: { Authorization: 'Bearer xxx' }
});

createAdapterResponse

帮助适配器作者构造 FetchAdapterResponse:

import { createAdapterResponse } from '@soybeanjs/fetch';

const response = createAdapterResponse({
  status: 200,
  statusText: 'OK',
  headers: new Headers({ 'content-type': 'application/json' }),
  body: responseBody
});

createUploadProgressAdapter

底层函数,创建基于 XHR 的上传进度适配器(详见 上传进度跟踪)。

单次请求推荐直接使用 onUploadProgress 配置项,无需手动创建适配器。

import { createUploadProgressAdapter } from '@soybeanjs/fetch';
import type { UploadProgressEvent } from '@soybeanjs/fetch';

const adapter = createUploadProgressAdapter((event: UploadProgressEvent) => {
  console.log(`${event.progress}% (${event.loaded}/${event.total})`);
});

// adapter 类型为 FetchAdapter | undefined
// 浏览器环境返回 FetchAdapter,Node.js 返回 undefined

📝 完整示例

带认证的 API 请求

import { createRequest } from '@soybeanjs/fetch';
import type { FetchResponse } from '@soybeanjs/fetch';

interface ApiResponse<T = any> {
  code: number;
  data: T;
  message: string;
}

interface User {
  id: number;
  name: string;
  email: string;
}

const request = createRequest(
  {
    baseURL: 'https://api.example.com',
    timeout: 10000
  },
  {
    transform: (response: FetchResponse<ApiResponse>) => {
      return response.data.data;
    },
    onRequest: async config => {
      const token = localStorage.getItem('token');
      if (token) {
        config.headers.set('Authorization', `Bearer ${token}`);
      }
      return config;
    },
    isBackendSuccess: response => {
      return response.data.code === 200;
    },
    onBackendFail: async (response, instance) => {
      const { code } = response.data;

      // Token 过期,刷新后重试
      if (code === 401) {
        const newToken = await refreshToken();
        localStorage.setItem('token', newToken);
        response.config.headers.set('Authorization', `Bearer ${newToken}`);
        return instance(response.config);
      }
    },
    onError: async error => {
      console.error(error.message);
    }
  }
);

// 1. 获取用户信息
async function getUser(id: number) {
  const user = await request<User>({ url: `/users/${id}` });
  return user;
}

// 2. 创建用户
async function createUser(data: Partial<User>) {
  const user = await request.post<User>('/users', data);
  return user;
}

// 3. 下载文件
async function downloadReport(reportId: string) {
  const fileData = await request.get('/download/report.pdf', {
    responseType: 'blob'
  });

  const url = URL.createObjectURL(fileData.file);
  const a = document.createElement('a');
  a.href = url;
  a.download = fileData.filename;
  a.click();
  URL.revokeObjectURL(url);
}

// 4. 上传文件
async function uploadFile(file: File) {
  const formData = new FormData();
  formData.append('file', file);

  return await request.post('/upload', formData);
}

🔧 API 参考

createRequest

创建标准请求实例。

function createRequest<ResponseData, ApiData>(
  config?: FetchRequestConfig,
  options?: RequestOption<ResponseData, ApiData>
): RequestInstance<ResponseData, ApiData>;

toFlatRequest

createRequest 创建的请求实例包装为扁平化请求实例,不抛出异常。扁平化实例复用原请求实例的 state / instance / 缓存与鉴权状态。

function toFlatRequest<ResponseData, ApiData>(
  request: RequestInstance<ResponseData, ApiData>
): FlatRequestInstance<ResponseData, ApiData>;

RequestInstance.withResponse

发起请求并返回转换后的数据 + 完整 FetchResponse。仍然抛错,供 toFlatRequest 等包装器使用。

interface RequestInstance<ResponseData = any, ApiData = ResponseData> {
  withResponse<T = ApiData, R extends ResponseType = 'json'>(
    config: FetchRequestConfig<R>
  ): Promise<{ data: MappedType<R, T>; response: FetchResponse<ResponseData> }>;
}

createFetch / $fetch

创建 ofetch 兼容的轻量 fetch 客户端。

function createFetch(defaults?: FetchRequestConfig): $Fetch;

interface $Fetch {
  <T = any, R extends ResponseType = 'json'>(
    request: string,
    options?: FetchRequestConfig<R>
  ): Promise<MappedType<R, T>>;
  raw<T = any, R extends ResponseType = 'json'>(
    request: string,
    options?: FetchRequestConfig<R>
  ): Promise<FetchResponse<MappedType<R, T>>>;
  native: typeof fetch;
  create(defaults: FetchRequestConfig): $Fetch;
}

createTypedClient

基于 openapi-typescript 生成的 paths 类型创建类型安全的客户端。从 @soybeanjs/fetch/openapi 子路径导入。

function createTypedClient<Paths, Prefix = '', Field = ''>(
  requestInstance: RequestInstance<any, any>,
  prefix?: Prefix
): TypedClient<Paths, Prefix, Field>;

toFlatTypedClient

创建类型安全的扁平化客户端,不抛出异常。接受的是普通 RequestInstance(与 createTypedClient 相同),内部通过 withResponse + try/catch 实现扁平化返回。从 @soybeanjs/fetch/openapi 子路径导入。

function toFlatTypedClient<Paths, Prefix = '', Field = ''>(
  requestInstance: RequestInstance<any, any>,
  prefix?: Prefix
): FlatTypedClient<Paths, Prefix, Field>;

createUploadProgressAdapter

创建上传进度适配器(底层函数,跨运行时)。浏览器使用 XHR,Node/Bun/Deno/CF 使用 TransformStream。返回 FetchAdapter,无可用方案时返回 undefined

大多数场景下直接使用 onUploadProgress 配置项即可(详见 上传进度跟踪),无需手动调用此函数。

function createUploadProgressAdapter(onUploadProgress: (event: UploadProgressEvent) => void): FetchAdapter | undefined;

类型定义

// 响应
interface FetchResponse<T = any> {
  data: T;
  status: number;
  statusText: string;
  headers: Headers;
  config: ResolvedFetchRequestConfig;
  request?: Request;
}

// 错误
class FetchError<T = any> extends Error {
  code?: string;
  config?: FetchRequestConfig;
  request?: Request;
  response?: FetchResponse<T>;
  get status(): number | undefined; // 别名 statusCode
  get statusText(): string | undefined; // 别名 statusMessage
  get data(): T | undefined;
}

class BackendError<ResponseData = any> extends FetchError<ResponseData> {
  // error.code === 'BACKEND_ERROR'
}

// 请求配置
interface FetchRequestConfig<R extends ResponseType = 'json'> extends Omit<
  RequestInit,
  'method' | 'headers' | 'body' | 'signal'
> {
  baseURL?: string;
  url?: string;
  method?: HttpMethod | string;
  headers?: Headers | Record<string, string>;
  query?: Record<string, any>;
  body?: BodyInit | Record<string, any> | null;
  responseType?: R;
  timeout?: number;
  signal?: AbortSignal;
  validateStatus?: (status: number) => boolean;
  paramsSerializer?: (params: Record<string, any>) => string;
  parseResponse?: (text: string) => any;
  getFileName?: (response: FetchResponse) => string;
  retry?: RetryOptions;
  adapter?: FetchAdapter;
  onUploadProgress?: (event: UploadProgressEvent) => void;
  ignoreResponseError?: boolean;
  // 传输层钩子(支持数组)
  onRequest?: FetchHook;
  onRequestError?: FetchHook;
  onResponse?: FetchHook;
  onResponseError?: FetchHook;
}

// 响应类型
type ResponseType = 'json' | 'auto' | 'blob' | 'arraybuffer' | 'stream' | 'text' | 'document';

// 适配器
type FetchAdapter = (url: string, init: FetchAdapterInit) => Promise<FetchAdapterResponse>;

// 上传进度事件
interface UploadProgressEvent {
  loaded: number; // 已上传字节数
  total: number; // 总字节数(不可计算时为 0)
  progress: number; // 进度百分比 0-100
  lengthComputable: boolean; // 总大小是否已知
}

❓ FAQ

与 @soybeanjs/request 的区别?

| 特性 | @soybeanjs/request | @soybeanjs/fetch | | ------------ | ------------------- | ----------------------------- | | 底层 | Axios | 原生 Fetch | | 运行时依赖 | axios, axios-retry | 零依赖 | | Headers | AxiosHeaders([]) | 原生 Headers(.set()/.get()) | | 重试 | axios-retry | 内置实现 | | 适配器 | 不支持 | ✅ 支持自定义适配器 | | $fetch API | 不支持 | ✅ 兼容 ofetch | | 传输层钩子 | 不支持 | ✅ onRequest/onResponse/... | | 自动响应检测 | 不支持 | ✅ responseType: 'auto' | | 业务 API | ✅ createRequest 等 | ✅ 完全兼容 |

如何从 @soybeanjs/request 迁移?

  1. 替换 import { ... } from '@soybeanjs/request'from '@soybeanjs/fetch'
  2. 替换 AxiosResponseFetchResponse
  3. 替换 AxiosErrorFetchError
  4. 替换 config.headers.Authorization = 'xxx'config.headers.set('Authorization', 'xxx')
  5. 替换 'axios-retry' 配置 → retry 配置
  6. instance.request(config)instance(config)(无 .request 方法)
  7. 替换 databody(请求体)、paramsquery(查询参数)

为什么需要两种请求使用方式?

  • 直接调用 request() / request.get()(抛出异常模式):适合大多数场景,请求失败会抛出异常,可使用 try-catch 捕获
  • toFlatRequest(request) 包装后调用(扁平化返回模式):适合需要统一处理成功和失败的场景,不会抛出异常,通过返回值 { data, error, response } 判断

什么时候用 $fetch,什么时候用 createRequest?

  • $fetch:简单的 HTTP 调用,不需要业务逻辑校验(如调用第三方 API)
  • createRequest:需要业务逻辑(如 isBackendSuccesstransformonBackendFail 重试)

如何实现请求取消?

const controller = new AbortController();

const promise = request({
  url: '/users',
  signal: controller.signal
});

// 取消请求(不会触发重试)
controller.abort();

📄 License

MIT License © 2026 SoybeanJS