@pluve/fetch2
v0.0.19
Published
fetch abort
Readme
@pluve/fetch2
基于 fetch 封装,保留与
@pluve/fetch基本一致的用法。若用到文件上传,请参考@pluve/file-utility或者@pluve/storage-client。
安装
pnpm add @pluve/fetch2引用
import FetchAgent from '@pluve/fetch2';使用说明
推荐优先使用以下三条主路径:
request/get/post:默认全局 runtime(兼容旧用法)createRuntime:实例化 runtime(隔离配置,适合多应用/SSR)createClient:按baseURL构建请求参数
快速开始(推荐)
import { createRuntime, RequestMethodsEnum } from '@pluve/fetch2';
const runtime = createRuntime({
baseURL: 'https://api.example.com',
requestSource: 'web-app',
timeout: 15000,
});
runtime.setupReqInterceptor((req) => {
req.headers = { ...(req.headers || {}), Authorization: 'Bearer token' };
return false;
});
const data = await runtime.request({
url: '/user/profile',
method: RequestMethodsEnum.GET,
retry: { enabled: true, retries: 2 },
});本次优化概览(性能与鲁棒性)
- 重试稳定性:为每次重试重新创建
AbortController与signal,避免前一次中止影响后续尝试(此前会出现重试立即失败的问题)。 - GET 查询参数升级:支持嵌套对象与数组的编码策略(
dot/bracket/json),并提供最大递归深度限制,兼顾易读与兼容后端解析。 - 文本与流处理更清晰:在响应处理阶段统一基于
Content-Type选择解析方式,并保留responseType强制覆盖;文本响应支持流式增量读取与可选自动 JSON 解析。 - 异常信息更详尽:对 4xx/5xx 响应提取
Retry-After(秒或 HTTP-Date),写入异常data前缀,便于重试策略准确参考。 - 代码与导入小清理:移除未使用的解析函数导入,保持打包与运行更干净。
GET 查询参数(query)增强
在 GET 请求中,新增可选的查询编码策略与最大深度:
await FetchAgent.get({
url: '/api/search',
method: RequestMethodsEnum.GET,
query: { k: '中文 空格', filters: { price: { min: 10, max: 100 }, tags: ['a','b'] } },
queryEncodeStrategy: 'bracket', // 'dot' | 'bracket' | 'json'
queryEncodeMaxDepth: 4,
});
// 'bracket' 示例:/api/search?k=%E4%B8%AD%E6%96%87%20%E7%A9%BA%E6%A0%BC&filters[price][min]=10&filters[price][max]=100&filters[tags][]=a&filters[tags][]=b当 queryEncodeStrategy 为 json 或超过 queryEncodeMaxDepth 时,复杂对象会被 JSON.stringify 后作为单字段值,方便后端统一解析。
更多示例:
// dot 策略:以点号展开嵌套结构
await FetchAgent.get({
url: '/api/search',
method: RequestMethodsEnum.GET,
query: { filters: { price: { min: 10, max: 100 }, tags: ['a','b'] } },
queryEncodeStrategy: 'dot',
});
// 实际请求:/api/search?filters.price.min=10&filters.price.max=100&filters.tags=a&filters.tags=b
// json 策略:原样字符串化复杂对象,后端统一按 JSON 解析
await FetchAgent.get({
url: '/api/search',
method: RequestMethodsEnum.GET,
query: { filters: { price: { min: 10, max: 100 }, tags: ['a','b'] } },
queryEncodeStrategy: 'json',
});
// 实际请求:/api/search?filters=%7B%22price%22%3A%7B%22min%22%3A10%2C%22max%22%3A100%7D%2C%22tags%22%3A%5B%22a%22%2C%22b%22%5D%7DGET 查询参数(query)
在 GET 请求中,使用 requestInfo.query 显式传递查询参数,内部通过 URLSearchParams 进行编码:
await FetchAgent.get({ url: '/api/list', method: RequestMethodsEnum.GET, query: { q: '中文 空格', tags: ['a','b'] } });
// 实际请求:/api/list?q=%E4%B8%AD%E6%96%87%20%E7%A9%BA%E6%A0%BC&tags=a&tags=b注意:null/undefined 会被忽略;数组将作为多个同名键编码。
响应解析(responseType)与异常结构统一
支持通过 requestInfo.responseType 强制响应解析方式,取值:json | text | blob | arrayBuffer | ndjson | sse。默认仍基于 Content-Type 自动解析。
当使用 responseType: 'ndjson' 时,库会按行解析响应文本的每一行:
- 空行跳过;
- 可被
JSON.parse的行解析为对象; - 非法 JSON 行会以原始字符串返回,避免整体失败;
你还可以通过 transformResponse(data, response, request) 在成功响应后对数据做二次转换或映射:
await FetchAgent.get({
url: '/stream',
responseType: 'ndjson',
transformResponse: (data) => data.filter(Boolean),
// 流式逐行
onNdjsonLine: (item, raw) => {
// item: JSON对象或原始字符串;raw: 原始行文本
console.log('line:', item);
},
});当响应头为 Content-Type: text/event-stream(或显式指定 responseType: 'sse')时,将按 SSE 协议逐事件解析:
- 事件块以空行分隔;
- 识别
event:、data:、id:字段; - 若
data:"..."为 JSON 字符串,库会自动尝试JSON.parse;否则按原始字符串返回;
await FetchAgent.get({
url: '/sse/stream',
method: RequestMethodsEnum.GET,
// 可省略,若服务端返回 text/event-stream 会自动启用
// responseType: 'sse',
onSseEvent: (evt) => {
// evt: { event: string; data: any; id: string | null; raw: string }
if (evt.event === 'reply' && evt.data?.payload) {
console.log('reply content:', evt.data.payload.content);
}
},
});非 2xx 响应将返回统一异常结构 IExceptionInfo,并包含结构化 details 字段(如 status、retryAfterMs、isRetryable)。为兼容旧逻辑,data 仍保留字符串格式(如 status=<code>;...)。
同时,文本响应也支持流式读取:
await FetchAgent.get({
url: '/large-text',
responseType: 'text',
onTextChunk: (chunk) => {
// 每次接收到一段文本块
console.log('chunk:', chunk.length);
},
});SSE 流式响应示例:
await FetchAgent.get({
url: '/sse/stream',
method: RequestMethodsEnum.GET,
onSseEvent: (evt) => {
// 每次接收到一个事件块触发一次
console.log('event:', evt.event, 'id:', evt.id);
},
});可选重试机制(指数退避 + 抖动)
仅对幂等方法(GET/HEAD/OPTIONS)支持重试。可通过 retry 字段开启:
await FetchAgent.get({
url: '/api/list',
method: RequestMethodsEnum.GET,
retry: {
enabled: true,
retries: 3, // 最大重试次数(不含首次)
baseDelayMs: 200, // 基准退避毫秒
maxDelayMs: 2000, // 最大退避毫秒
jitter: 'full', // 抖动策略:none/full
retryOn5xx: true, // 5xx 响应参与重试
retryOnNetworkError: true, // 网络错误/超时参与重试
retryOn429: true, // 429(Too Many Requests)参与重试
// methods: [RequestMethodsEnum.GET], // 可选,默认使用幂等方法集
},
});重试触发条件:
- 网络错误 / 超时(异常码 10000/10001/10002)
- HTTP 5xx(异常码为
RESPONSE_OTHER,且data包含status=5xx;) - HTTP 429(异常码通常为
RESPONSE_400_PLUS,data包含status=429;)
非幂等方法(如 POST/PUT/PATCH/DELETE)默认不启用重试,除非明确指定 methods 并了解风险。
对于 429 与 5xx 响应,若开启对应重试(retryOn429/retryOn5xx),将优先使用 Retry-After 头来计算等待时间(支持秒数字符串与 HTTP-Date),并受 maxDelayMs 上限约束;若无该头或无法解析,则回退至指数退避策略。
当响应头包含 Retry-After 时,重试等待时间优先采用该值,受 maxDelayMs 上限约束(支持秒与 HTTP-Date):
- 若为秒数字符串(如
Retry-After: 3),直接按秒数等待; - 若为 HTTP-Date(RFC 7231 格式,如
Wed, 21 Oct 2015 07:28:00 GMT),将转换为与当前时间的差值(秒)后等待; 若未提供则回退为指数退避策略。
示例:服务端返回 429/503 且 Retry-After 为未来日期,客户端等待约 1s 后重试。
重试的稳定性修复:每次重试都会创建新的 AbortController 与 signal,避免上一次因 abort() 或超时导致后续尝试立即失败的情况。
导出方式
提供具名导出,亦保留默认导出,方便按需引入:
import { request, get, post, createRuntime, abortPendingRequestByKey } from '@pluve/fetch2';
import FetchAgent from '@pluve/fetch2';ESM 导入示例
当前包默认发布 ESM(exports.import),推荐使用 ESM 导入:
// ESM(现代前端/Node 环境)
import FetchAgent, { request, get, post, createRuntime, RequestMethodsEnum } from '@pluve/fetch2';
// 类型(TypeScript)
// 自动从 package.json 的 types 字段解析,无需额外配置若你的项目仍为 CJS,请通过构建工具转译后再消费本包,不建议直接 require('@pluve/fetch2')。
类型说明
IRequestParams
interface RequestInit {
/** A BodyInit object or null to set request's body. */
body?: BodyInit | null;
/** A string indicating how the request will interact with the browser's cache to set request's cache. */
cache?: RequestCache;
/** A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials. */
credentials?: RequestCredentials;
/** A Headers object, an object literal, or an array of two-item arrays to set request's headers. */
headers?: HeadersInit;
/** A cryptographic hash of the resource to be fetched by request. Sets request's integrity. */
integrity?: string;
/** A boolean to set request's keepalive. */
keepalive?: boolean;
/** A string to set request's method. */
method?: string;
/** A string to indicate whether the request will use CORS, or will be restricted to same-origin URLs. Sets request's mode. */
mode?: RequestMode;
/** A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */
redirect?: RequestRedirect;
/** A string whose value is a same-origin URL, "about:client", or the empty string, to set request's referrer. */
referrer?: string;
/** A referrer policy to set request's referrerPolicy. */
referrerPolicy?: ReferrerPolicy;
/** An AbortSignal to set request's signal. */
signal?: AbortSignal | null;
/** Can only be null. Used to disassociate request from any Window. */
window?: null;
}
export interface IRequestParams extends RequestInit {
key?: string; // 接口请求标识,用于取消请求
url: string; // 接口请求链接
method?: RequestMethodsEnum; // 请求方式
headers?: ICustomHeader; // 接口请求头,可与全局的请求头合并
useGlobalHeader?: boolean; // 是否使用全局请求头
body?: any; // 请求体
query?: Record<string, any>; // GET 查询参数对象(支持嵌套对象/数组)
queryEncodeStrategy?: 'dot' | 'bracket' | 'json'; // GET 查询编码策略
queryEncodeMaxDepth?: number; // GET 查询编码最大递归深度(超出后按 json 处理)
submitDataType?: 'json' | 'form'; // 数据提交类型,支持application/json 和 application/x-www-form-urlencoded
timeout?: number; // 接口超时时间,单位毫秒,默认15000
timeStamp?: number; // 接口发起时的时间戳
abortController?: AbortController; // 取消请求的控制器
reqInterceptor?: IRequestInterceptor; // 请求拦截器
respInterceptor?: IResponseInterceptor; // 响应拦截器
[key: string]: any; // 支持携带自定义请求参数,可在拦截器中使用
}Response
interface Body {
readonly body: ReadableStream<Uint8Array> | null;
readonly bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
text(): Promise<string>;
}
interface Response extends Body {
readonly headers: Headers;
readonly ok: boolean;
readonly redirected: boolean;
readonly status: number;
readonly statusText: string;
readonly type: ResponseType;
readonly url: string;
clone(): Response;
}IExceptionInfo
export interface IExceptionInfo {
code: number; // EXCEPTION_KEYS
message: string;
data?: string | number;
details?: {
status?: number;
retryAfterMs?: number;
responseType?: string;
responseSize?: number;
isRetryable?: boolean;
reason?: string;
};
}
const EXCEPTION_KEYS = {
ABORT_EXCEPTION_KEY: 10000, // 请求终止
TIMEOUT_EXCEPTION_KEY: 10001, // 请求超时
NETWORK_ERROR_EXCEPTION_KEY: 10002, // 请求网络异常
INTERCEPT_AT_GLOBAL_LEVEL: 10003, // 被全局拦截器拦截
INTERCEPT_AT_API_LEVEL: 10004, // 被接口拦截器拦截
REQUEST_REDIRECT: 10005, // 请求重定向
RESPONSE_400_PLUS: 10006, // 400相关响应
RESPONSE_OTHER: 10007, // 其他异常
REQUEST_CROSS: 10008, // 请求跨域
RESPONSE_TIME_TRIGGER_THRESHOLD: 10009, // 接口耗时触发阀值
BUSINESS_EXCEPTION: 10010, // 业务异常(由业务拦截器识别)
};接口
setDebug
设置是否为 debug 模式。debug 模式下,控制台会打印详细的接口请求及响应信息,默认为关闭,可在开发测试模式下开启,建议在生产环境关闭。
FetchAgent.setDebug(true);setupTimeout
设置超时时间,默认为 15s,单位为毫秒
FetchAgent.setTimeout(15000);setupApiTimeCostThreshold
设置接口响应时长阀值,默认 1000ms,超过该值则会在控制台打印接口响应时长
FetchAgent.setupApiTimeCostThreshold(1000);setupDefaultGlobalHeaderGenerator
设置统一请求头信息,若某个接口请求头有特殊要求,可在 GET、POST 方法中设置
FetchAgent.setupDefaultGlobalHeaderGenerator(() => ({
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
}));setupDefaultCommonReqBodyGenerator
在请求体中设置通用参数,如鉴权信息等。若表单提交,则会忽略该方法设置的参数。
FetchAgent.setupDefaultCommonReqBodyGenerator(() => ({
openx_header: 'xxx',
}));setupReqInterceptor
接口请求全局拦截器,若返回 true,则终止请求。接口级别的拦截器优先级高于全局拦截器。
FetchAgent.setupReqInterceptor((req: IRequestParams) => false);setupRespInterceptor
响应结果全局拦截器,若返回 true,则终止后续流程。可用于 token 过期、异常处理等场景。接口级别的拦截器优先级高于全局拦截器。此处读取 response 响应数据应先 clone,否在 return false 后会导致响应体重复使用的异常。
FetchAgent.setupRespInterceptor((requestParams: IRequestParams, response: Response) => false);setupBusinessErrorInterceptor
设置业务异常统一拦截器。该拦截器在成功响应(HTTP 2xx)并完成解析/转换后执行:
返回IExceptionInfo表示识别到业务异常,将统一进入异常处理链(reject+setupOnError)。
返回false/null/undefined表示通过。
FetchAgent.setupBusinessErrorInterceptor((data, response, requestParams) => {
// 示例:后端约定 success=false 表示业务失败
if (data?.success === false) {
return {
code: 10010,
message: data.message || '业务异常',
data: data.message || '',
details: { reason: 'business_failed' },
};
}
return false;
});setupOnError
设置全局错误处理函数 接口响应码非 200 时会回调,统一异常结构通过
IExceptionInfo返回;
接口响应超时时会回调;
接口响应时长超过警告阀值时会回调;
可在异常日志上报场景下使用。
FetchAgent.setupOnError((err: IExceptionInfo, requestInfo: IRequestParams) => {
// 统一异常结构
// err.code 参见 EXCEPTION_KEYS
// err.message 为预定义文案
// err.data 对于 HTTP 非 2xx 响应将包含 "status=<code>;" 以及服务端返回内容(JSON 字符串或文本),二进制响应则记录类型与大小
console.log(err, requestInfo);
});sendRequest
发送请求
① 返回 Promise;若请求被拦截,则返回 rejected Promise,异常结构见IExceptionInfo。
② 支持 GET、POST、PUT、DELETE、PATCH 等请求方式。
③ 支持请求头、请求体、请求参数、请求超时、请求取消等功能。
④ 支持传入泛型,返回指定类型的数据。
/**
* 发送请求
* @param requestInfo
*/
export const request = <T = unknown>(requestInfo: IRequestParams): Promise<T> => {
const requestParams = buildRequestOptions(requestInfo);
if (requestParams.reqInterceptor && requestParams.reqInterceptor(requestParams)) {
return Promise.reject(ExceptionUtil.generateExceptionByKey(ExceptionUtil.EXCEPTION_KEYS.INTERCEPT_AT_API_LEVEL));
}
if (handlerBeforeRequest(requestParams)) {
return Promise.reject(ExceptionUtil.generateExceptionByKey(ExceptionUtil.EXCEPTION_KEYS.INTERCEPT_AT_GLOBAL_LEVEL));
}
requestMap.set(requestParams.key, requestParams);
const apiPromise = doRequest(requestParams);
return handlerApiResponse<T>(requestParams, apiPromise);
};
// 业务服务声明
export const fetchArticle = ({ pageNo, pageSize }: { pageNo: number; pageSize: number }) =>
FetchAgent.sendPost<ApiCommonResponse<IArticleDataItem[]>>({
url: articleService,
body: {
article: { articleType: null, keyword: null },
pageNo,
pageSize,
},
respInterceptor: async (requestParams, response) => {
console.log('respInterceptor: ', requestParams);
// 一定要这样写
// console.log(response.clone().json());
// 这样写报错
// console.log(response.json());
return false;
},
});
// 业务调用
try {
const articleListResp = await fetchArticle({ pageNo: this.pageNo, pageSize: this.pageSize });
this.updateArticles(refresh, articleListResp);
} catch (e) {
this.showError();
}为了兼容 @pluve/fetch,保留以下别名方法(建议优先使用 request/get/post):
| 编号 | 方法名 | 说明 |
| -- | -- | -- |
| 1 | createRuntime | 创建隔离 runtime(推荐) |
| 2 | request / sendRequest | 发送接口请求 |
| 3 | get / post | 常用 GET/POST 请求 |
| 4 | sendRequestCustomer / sendRequestPure | 兼容别名,建议迁移 |
| 5 | sendGet / sendPost | 兼容别名,建议迁移 |
abortPendingRequestByKey
根据 key 终止请求
export const abortPendingRequestByKey = (key: string) => {
if (!key) {
return;
}
const requestItem = requestMap.get(key) as IRequestParams;
if (requestItem && requestItem.abortController) {
requestAbortManualFlag.set(key, true);
requestItem.abortController.abort();
requestMap.delete(key);
}
};
FetchAgent.abortPendingRequestByKey('key');clearRequest
清除 pending 中的请求
export const clearRequest = () => {
// 只要还在此map中的请求,均为pending状态
requestMap.forEach((requestItem) => {
if (requestItem.abortController) {
const key = requestItem.key || '';
requestAbortManualFlag.set(key, true);
requestItem.abortController.abort();
}
});
requestMap.clear();
};
FetchAgent.clearRequest();外部依赖
若低版本浏览器需要依赖 whatwg-fetch
issues 说明
暂无版本计划
