Skip to content

Repository files navigation

@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';importtype{FetchResponse}from'@soybeanjs/fetch';interfaceApiResponse<T=any>{code: number;data: T;message: string;}// 创建请求实例constrequest=createRequest({baseURL: 'https://api.example.com',timeout: 10000},{// 转换响应数据// !!!注意这里一定要给 response 指定类型,这样才能有类型推导transform: (response: FetchResponse<ApiResponse>)=>{returnresponse.data.data;},// 请求前拦截onRequest: asyncconfig=>{// 添加 token(headers 是原生 Headers 实例,使用 .set())config.headers.set('Authorization',`Bearer ${getToken()}`);returnconfig;},// 判断后端业务是否成功isBackendSuccess: response=>{returnresponse.data.code===200;},// 后端业务失败处理onBackendFail: async(response,instance)=>{// 处理 token 过期等情况if(response.data.code===401){awaitrefreshToken();// 重新发起请求(走完整管道)returninstance(response.config);}},// 错误处理onError: asyncerror=>{console.error('Request failed:',error.message);}});// 发起请求constdata=awaitrequest({url: '/users',method: 'GET'});

扁平化响应实例 (toFlatRequest)

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

import{createRequest,toFlatRequest}from'@soybeanjs/fetch';constrequest=createRequest({baseURL: 'https://api.example.com'},options);constflatRequest=toFlatRequest(request);const{ data, error, response }=awaitflatRequest({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 请求constuser=await$fetch<User>('/api/users/1');// POST 请求constcreated=await$fetch<User>('/api/users',{method: 'POST',body: {name: 'John'}});// 创建带默认值的实例constapiFetch=$fetch.create({baseURL: 'https://api.example.com',headers: {Authorization: 'Bearer xxx'},retry: {retries: 3}});// 获取完整响应(不抛异常)constresponse=await$fetch.raw('/api/users/1');console.log(response.status,response.data);// 直接访问原生 fetch$fetch.native('https://example.com');

📖 核心概念

RequestOption 配置项

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

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

传输层钩子(对标 ofetch)

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

constrequest=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{awaitrequest({url: '/users/1'});}catch(error){if(errorinstanceofBackendError){// 业务错误,例如 code !== 200console.error('业务错误:',error.message);}elseif(errorinstanceofFetchError){// 网络 / HTTP 错误console.error('网络错误:',error.message);}}// 或通过 code 判别(等价于 instanceof BackendError)if(fetchError.code===BACKEND_ERROR_FLAG){// ...}

FetchError 提供以下便捷属性:

属性说明
statusHTTP 状态码(别名 statusCode)
statusTextHTTP 状态文本(别名 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. 文件下载

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

// 下载文件constfileData=awaitrequest({url: '/download/report.pdf',method: 'GET',responseType: 'blob'});// fileData 包含:// {// file: Blob,// filename: 'report.pdf',// contentType: 'application/pdf'// }// 自定义文件名解析constfileData=awaitrequest({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(默认),需要添加一个泛型参数指定业务数据类型,其他类型无需指定interfaceUserData{id: number;name: string;}constdata=awaitrequest<UserData>({url: '/users/123'});// auto — 根据 Content-Type 自动检测(推荐用于 $fetch)constdata=await$fetch('/api/data',{responseType: 'auto'});// 文本consttext=awaitrequest({url: '/data.csv',responseType: 'text'});// HTML/XML 文档(使用 DOMParser)constdoc=awaitrequest({url: '/template.html',responseType: 'document'});// Blob(文件)constfile=awaitrequest({url: '/download/image.png',responseType: 'blob'});// ArrayBufferconstbuffer=awaitrequest({url: '/download/data.bin',responseType: 'arraybuffer'});// Stream(SSE / 大文件流)conststream=awaitrequest({url: '/events',responseType: 'stream'});

支持的响应类型:

类型说明
jsonJSON(默认)
auto根据 Content-Type 自动检测
text文本
blobBlob 文件
arraybufferArrayBuffer
streamReadableStream(含 SSE text/event-stream)
documentHTML/XML 文档(DOMParser,浏览器环境)

3. 状态管理

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

constrequest=createRequest({baseURL: 'https://api.example.com'},{// ...其他配置onRequest: config=>{config.headers.set('Authorization',`Bearer ${request.state.token}`);returnconfig;}});// 用户自定义状态 —— 直接在 state 上读写request.state.token='new-token';request.state.userId=123;// 内置运行时状态也可访问request.state.loading.count;// 当前并发请求数request.state.cache.size;// 缓存条目数

消息去重

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

constrequest=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. 自动重试

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

constrequest=createRequest({baseURL: 'https://api.example.com',retry: {retries: 3,retryDelay: retryCount=>retryCount*1000,retryCondition: error=>{// 仅在网络错误或 5xx 错误时重试return!error.response||error.response.status>=500;}}},options);
配置项类型默认值说明
retriesnumber0重试次数
retryDelay(count, error) => number线性退避重试延迟(毫秒)
retryCondition(error) => boolean | Promise<boolean>网络错误+重试状态码重试条件

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

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

5. 超时控制

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

constrequest=createRequest({timeout: 10000},options);// 超时会抛出 FetchError,code 为 'ERR_TIMEOUT'// 错误消息格式: [GET] "https://...": Request timeout of 10000ms exceeded// 用户主动取消(不会重试)constcontroller=newAbortController();constpromise=request({url: '/users',signal: controller.signal});// 取消请求controller.abort();

6. 类型推导

完整的 TypeScript 类型支持:

interfaceUser{id: number;name: string;}interfaceApiResponse<T=any>{code: number;data: T;message: string;}// ResponseData:后端原始响应类型// ApiData:业务数据类型constrequest=createRequest({baseURL: 'https://api.example.com'},{transform: (response: FetchResponse<ApiResponse>)=>response.data.data});// 类型推导:data 的类型是 ApiResponse<User>constuser=awaitrequest<User>({url: '/users/123'});

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

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

与普通 request() 的区别:

方法返回值是否经过 transform
request()转换后的业务数据✅ 是
request.raw()完整的 FetchResponse 对象❌ 否
// 1. 获取响应头中的自定义信息constresponse=awaitrequest.raw<User[]>({url: '/users',method: 'GET'});consttotalCount=response.headers.get('x-total-count');constrequestId=response.headers.get('x-request-id');conststatusCode=response.status;// 2. 文件下载时获取原始响应 + 文件信息constfileResponse=awaitrequest.raw({url: '/download/report.pdf',responseType: 'blob'});// fileResponse.data 包含 { file, filename, contentType }

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

8. 便捷 HTTP 方法

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

// GET 请求constusers=awaitrequest.get<User[]>('/users');constusersWithQuery=awaitrequest.get<User[]>('/users',{query: {page: 1,pageSize: 10}});// POST 请求constnewUser=awaitrequest.post<User>('/users',{name: '张三',email: 'zhangsan@example.com'});// PUT 请求constupdatedUser=awaitrequest.put<User>('/users/123',{name: '张三(已更新)'});// PATCH 请求constpatchedUser=awaitrequest.patch<User>('/users/123',{email: 'newemail@example.com'});// DELETE 请求awaitrequest.delete('/users/123');

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

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

import{createRequest,createAdapterResponse}from'@soybeanjs/fetch';// uniapp 适配器示例constuniappAdapter=async(url,init)=>{constres=awaituni.request({
url,method: init.methodasany,header: Object.fromEntries(init.headers.entries()),data: init.body,responseType: 'arraybuffer'});returncreateAdapterResponse({status: res.statusCode,statusText: '',headers: newHeaders(res.header),body: res.datainstanceofArrayBuffer ? res.data : newArrayBuffer(0)});};constrequest=createRequest({baseURL: 'https://api.example.com',adapter: uniappAdapter},options);

10. ignoreResponseError — 忽略响应错误

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

constresponse=await$fetch.raw('/api/users/404',{ignoreResponseError: true});// 即使是 404 也会返回 response,不会抛出 FetchErrorconsole.log(response.status);// 404console.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';importtype{paths}from'./openapi.d.ts';constrequest=createRequest({baseURL: 'https://api.example.com'},{/* ... */});// Field = 'data' 用于解包 envelope 结构constclient=createTypedClient<paths,'/api/v1','data'>(request,'/api/v1');// 路径、参数、请求体、返回值均有类型推导constmenus=awaitclient.get('/menu/list',{query: {page: 1,pageSize: 10}});// POST 请求constloginResult=awaitclient.post('/auth/login',{body: {username: 'admin',password: '123456'}});// 路径参数(替换 URL 中的 {id})constuser=awaitclient.get('/users/{id}',{pathParams: {id: 1}});// raw 方法 — 跳过 transform,返回完整 FetchResponseconstresponse=awaitclient.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';constrequest=createRequest({baseURL: 'https://api.example.com'},{/* ... */});constflatClient=toFlatTypedClient<paths,'/api/v1','data'>(request,'/api/v1');const{ data, error }=awaitflatClient.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 / DenoTransformStream 字节计数已知大小 body 精确(Blob/ArrayBuffer/string 等)
CF WorkersTransformStream 字节计数可能缓冲(进度瞬间跳 100%)

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

基本用法(单次请求)

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

import{createRequest}from'@soybeanjs/fetch';constrequest=createRequest({baseURL: 'https://api.example.com'},{/* ... */});asyncfunctionuploadFile(file: File){constformData=newFormData();formData.append('file',file);// 只需在请求配置中传入 onUploadProgress 回调constresult=awaitrequest.post('/upload',formData,{onUploadProgress: ({ loaded, total, progress })=>{console.log(`上传进度: ${progress}% (${loaded}/${total} bytes)`);}});returnresult;}

$fetch 同样支持:

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

进度事件

onUploadProgress 回调接收一个 UploadProgressEvent 对象:

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

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

在 Vue / React 中结合进度条

import{ref}from'vue';import{createRequest,toFlatRequest}from'@soybeanjs/fetch';constuploadProgress=ref(0);constrequest=toFlatRequest(createRequest({baseURL: 'https://api.example.com'},{/* ... */}));asyncfunctionhandleUpload(file: File){constformData=newFormData();formData.append('file',file);const{ data, error }=awaitrequest.post('/upload',formData,{onUploadProgress: ({ progress })=>{uploadProgress.value=progress;}});if(error){console.error('上传失败:',error.message);}else{console.log('上传成功:',data);}uploadProgress.value=0;// 重置}

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

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

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

高级:createUploadProgressAdapter

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

import{createRequest,createUploadProgressAdapter}from'@soybeanjs/fetch';constrequest=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 下载时触发回调。

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

// 单次请求awaitrequest.get('/large-file',{responseType: 'blob',onDownloadProgress: ({ progress, loaded, total, lengthComputable })=>{if(lengthComputable){console.log(`下载: ${progress}% (${loaded}/${total} bytes)`);}else{console.log(`已下载 ${loaded} bytes`);}}});// $fetch 也支持constblob=await$fetch('/video.mp4',{responseType: 'blob',onDownloadProgress: ({ progress })=>{progressBar.value=progress;}});

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

属性类型说明
loadednumber已下载字节数
totalnumber总字节数(从 Content-Length 获取,无则为 0)
progressnumber下载百分比 0-100(不可计算时为 0)
lengthComputableboolean总大小是否已知

14. 请求缓存

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

constrequest=createRequest({baseURL: '/api',cache: {ttl: 30000,// 缓存 30 秒methods: ['get'],// 仅缓存 GET(默认)max: 100// 最多 100 条(默认),超出淘汰最旧// key: (config) => '...' // 自定义缓存 key}},options);// 第一次请求:发起网络调用consta=awaitrequest.get('/user');// 30 秒内的第二次请求:直接返回缓存constb=awaitrequest.get('/user');// 单次请求跳过缓存constc=awaitrequest.get('/user',{cache: false});

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

// 清除所有缓存request.clearCache();// 删除指定 key 的缓存(默认 key 格式为 "METHOD:url:query")request.deleteCache('GET:/api/user:id=1');// 更新数据后清除缓存,下次请求将重新拉取awaitrequest.put('/user',newData);request.clearCache();

15. 请求去重

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

constrequest=createRequest({baseURL: '/api',dedupe: true},options);// 两个并发相同请求 → 只发一次网络调用const[a,b]=awaitPromise.all([request.get('/user'),request.get('/user')]);console.log(a===b);// true(同一个响应)// 自定义去重 keyconstrequest2=createRequest({baseURL: '/api',dedupe: {key: config=>`${config.method}:${config.url}`}},options);

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

16. 并发限制

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

constrequest=createRequest({baseURL: '/api',concurrency: {maxConcurrent: 6// 最多同时 6 个请求}},options);// 批量上传 100 个文件,但最多同时上传 6 个constfiles=[...];// 100 filesconstresults=awaitPromise.all(files.map(file=>request.post('/upload',file)));

17. 全局 Loading 与慢请求追踪

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

constrequest=createRequest({baseURL: '/api',// 全局 loading:第一个请求开始时 true,最后一个请求结束时 falseonGlobalLoadingChange: loading=>{store.globalLoading=loading;},// 慢请求:超过 10 秒触发告警slowThreshold: 10000,onSlowRequest: ({ url, method, duration })=>{console.warn(`慢请求: ${method}${url} 已耗时 ${duration}ms`);// 上报监控...}},options);// 单请求 loading:该请求开始时 true,结束时 falseawaitrequest.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)
slowThresholdnumber慢请求阈值(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;// 忽略节流拒绝throwerr;});});

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

19. Auth 管理

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

constrequest=createRequest({baseURL: '/api',auth: {// 自动附加 Authorization: Bearer <token>getToken: ()=>localStorage.getItem('token'),// 刷新 token,返回新 tokenrefreshToken: async()=>{constres=awaitfetch('/auth/refresh',{headers: {'refresh-token': localStorage.getItem('refreshToken')}});const{ token }=awaitres.json();localStorage.setItem('token',token);returntoken;},// 何时触发刷新 —— 默认 401,可自定义状态码或判断函数// refreshOn: 403, // 403 时刷新refreshOn: (status,response)=>{// 自定义判断returnstatus===401||response.headers.get('x-token-expired')==='1';},// 刷新失败时调用(如跳转登录页)onUnauthorized: ()=>{router.push('/login');}}},options);
属性类型说明
getToken() => string | Promise<string>获取当前 token,自动附加到请求头
refreshToken() => Promise<string>刷新 token,返回新 token
refreshOnnumber | (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 等)constUserSchema=z.object({id: z.number(),name: z.string(),email: z.string().email()});constuser=awaitrequest.get('/user',{schema: UserSchema});// user 已通过运行时验证,类型安全// 使用普通验证函数(轻量 escape hatch)constuser2=awaitrequest.get('/user',{schema: data=>{if(!data.id)thrownewError('Missing id');returndata;}});

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

import{ERR_SCHEMA}from'@soybeanjs/fetch';try{awaitrequest.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';constrequest=createRequest({baseURL: '/api',// 发送前:camelCase → snake_casetransformRequest: body=>{if(typeofbody!=='object'||body===null)returnbody;returnObject.fromEntries(Object.entries(body).map(([k,v])=>[snakeCase(k),v]));},// 接收后:snake_case → camelCasetransformResponse: data=>{if(typeofdata!=='object'||data===null)returndata;returnObject.fromEntries(Object.entries(data).map(([k,v])=>[camelCase(k),v]));}},options);// 请求发送 { user_name: 'test' } (而非 { userName: 'test' })// 响应自动转为 { userName: 'test' } (而非 { user_name: 'test' })awaitrequest.post('/user',{userName: 'test'});

🛠️ 实用工具

parseContentDisposition

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

import{parseContentDisposition}from'@soybeanjs/fetch';constfilename=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';// 使用默认实例constdata=await$fetch('/api/users/1');// 创建自定义实例constapiFetch=createFetch({baseURL: 'https://api.example.com',retry: {retries: 3},onRequest: [({ request })=>console.log('→',request)]});// 链式创建constauthFetch=apiFetch.create({headers: {Authorization: 'Bearer xxx'}});

createAdapterResponse

帮助适配器作者构造 FetchAdapterResponse:

import{createAdapterResponse}from'@soybeanjs/fetch';constresponse=createAdapterResponse({status: 200,statusText: 'OK',headers: newHeaders({'content-type': 'application/json'}),body: responseBody});

createUploadProgressAdapter

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

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

import{createUploadProgressAdapter}from'@soybeanjs/fetch';importtype{UploadProgressEvent}from'@soybeanjs/fetch';constadapter=createUploadProgressAdapter((event: UploadProgressEvent)=>{console.log(`${event.progress}% (${event.loaded}/${event.total})`);});// adapter 类型为 FetchAdapter | undefined// 浏览器环境返回 FetchAdapter,Node.js 返回 undefined

📝 完整示例

带认证的 API 请求

import{createRequest}from'@soybeanjs/fetch';importtype{FetchResponse}from'@soybeanjs/fetch';interfaceApiResponse<T=any>{code: number;data: T;message: string;}interfaceUser{id: number;name: string;email: string;}constrequest=createRequest({baseURL: 'https://api.example.com',timeout: 10000},{transform: (response: FetchResponse<ApiResponse>)=>{returnresponse.data.data;},onRequest: asyncconfig=>{consttoken=localStorage.getItem('token');if(token){config.headers.set('Authorization',`Bearer ${token}`);}returnconfig;},isBackendSuccess: response=>{returnresponse.data.code===200;},onBackendFail: async(response,instance)=>{const{ code }=response.data;// Token 过期,刷新后重试if(code===401){constnewToken=awaitrefreshToken();localStorage.setItem('token',newToken);response.config.headers.set('Authorization',`Bearer ${newToken}`);returninstance(response.config);}},onError: asyncerror=>{console.error(error.message);}});// 1. 获取用户信息asyncfunctiongetUser(id: number){constuser=awaitrequest<User>({url: `/users/${id}`});returnuser;}// 2. 创建用户asyncfunctioncreateUser(data: Partial<User>){constuser=awaitrequest.post<User>('/users',data);returnuser;}// 3. 下载文件asyncfunctiondownloadReport(reportId: string){constfileData=awaitrequest.get('/download/report.pdf',{responseType: 'blob'});consturl=URL.createObjectURL(fileData.file);consta=document.createElement('a');a.href=url;a.download=fileData.filename;a.click();URL.revokeObjectURL(url);}// 4. 上传文件asyncfunctionuploadFile(file: File){constformData=newFormData();formData.append('file',file);returnawaitrequest.post('/upload',formData);}

🔧 API 参考

createRequest

创建标准请求实例。

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

toFlatRequest

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

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

RequestInstance.withResponse

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

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

createFetch / $fetch

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

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

createTypedClient

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

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

toFlatTypedClient

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

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

createUploadProgressAdapter

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

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

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

类型定义

// 响应interfaceFetchResponse<T=any>{data: T;status: number;statusText: string;headers: Headers;config: ResolvedFetchRequestConfig;request?: Request;}// 错误classFetchError<T=any>extendsError{code?: string;config?: FetchRequestConfig;request?: Request;response?: FetchResponse<T>;getstatus(): number|undefined;// 别名 statusCodegetstatusText(): string|undefined;// 别名 statusMessagegetdata(): T|undefined;}classBackendError<ResponseData=any>extendsFetchError<ResponseData>{// error.code === 'BACKEND_ERROR'}// 请求配置interfaceFetchRequestConfig<RextendsResponseType='json'>extendsOmit<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;}// 响应类型typeResponseType='json'|'auto'|'blob'|'arraybuffer'|'stream'|'text'|'document';// 适配器typeFetchAdapter=(url: string,init: FetchAdapterInit)=>Promise<FetchAdapterResponse>;// 上传进度事件interfaceUploadProgressEvent{loaded: number;// 已上传字节数total: number;// 总字节数(不可计算时为 0)progress: number;// 进度百分比 0-100lengthComputable: boolean;// 总大小是否已知}

❓ FAQ

与 @soybeanjs/request 的区别?

特性@soybeanjs/request@soybeanjs/fetch
底层Axios原生 Fetch
运行时依赖axios, axios-retry零依赖
HeadersAxiosHeaders([])原生 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 重试)

如何实现请求取消?

constcontroller=newAbortController();constpromise=request({url: '/users',signal: controller.signal});// 取消请求(不会触发重试)controller.abort();

📄 License

MIT License © 2026 SoybeanJS

About

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

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages