enhance-axios 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,395 @@
1
+ # enhance-axios
2
+
3
+ axios 增强库,提供防重复提交、请求取消、失败重试三个核心能力。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ npm install enhance-axios axios
9
+ ```
10
+
11
+ ```ts
12
+ import { createEnhanceInstance } from 'enhance-axios';
13
+
14
+ const api = createEnhanceInstance({ baseURL: '/api' });
15
+
16
+ // POST/PUT/PATCH/DELETE 默认启用防重复
17
+ api.post('/submit', { name: 'test' });
18
+
19
+ // GET 默认启用取消请求
20
+ api.get('/search', { q: 'keyword' });
21
+
22
+ // 失败自动重试
23
+ api.get('/data');
24
+ ```
25
+
26
+ ## 三个核心功能
27
+
28
+ ### 1. 防重复提交 (preventDuplicate)
29
+
30
+ **场景**:用户快速点击提交按钮,只保留第一次请求,后续重复请求复用第一次的结果。
31
+
32
+ ```ts
33
+ api.post('/submit', data, {
34
+ preventDuplicate: {
35
+ intervalMs: 2000,
36
+ requestKey: '${method}-${url}-${data.userId}',
37
+ }
38
+ });
39
+
40
+ // 简写
41
+ api.post('/submit', data, { preventDuplicate: true });
42
+ api.post('/submit', data, { preventDuplicate: false });
43
+ ```
44
+
45
+ **内部流程**:
46
+ ```
47
+ 请求 A ──→ 注册 pending ──→ 发送 ──→ 成功 ──→ resolve deferred ──→ 清理
48
+ 请求 B ──→ 检测重复 ──→ 阻止 ──→ 返回 deferred.promise (B 等待 A 的结果)
49
+ ```
50
+
51
+ - 默认 `methods`: `['POST', 'PUT', 'PATCH', 'DELETE']`
52
+ - 默认 `intervalMs`: `1000`
53
+ - 未提供 `requestKey` 时自动根据 method/url/params/data 生成 hash 作为 key
54
+
55
+ ### 2. 取消请求 (cancelRequest)
56
+
57
+ **场景**:搜索框连续输入,自动取消旧请求,只保留最新请求。
58
+
59
+ ```ts
60
+ api.get('/search', { q: 'keyword' }, {
61
+ cancelRequest: { requestKey: '${method}-${url}' }
62
+ });
63
+ ```
64
+
65
+ **内部流程**:
66
+ ```
67
+ 请求 A ──→ 已发出
68
+ 请求 B ──→ 检测 pending ──→ 取消 A ──→ 注册 B ──→ 发送 (A 取消, B 完成)
69
+ ```
70
+
71
+ - 默认 `methods`: `['GET']`
72
+ - 使用 `AbortController.abort()` 取消 HTTP 请求
73
+
74
+ ### Content-Type 简化
75
+
76
+ 默认 `'json'`,自动设置 `Content-Type` 头并转换数据格式。
77
+
78
+ ```ts
79
+ // json(默认)→ 自动 JSON.stringify
80
+ api.post('/submit', { name: 'test' });
81
+
82
+ // form → 自动转 URLSearchParams
83
+ api.post('/login', { username: 'admin', password: '123' }, { contentType: 'form' });
84
+
85
+ // file → 自动转 FormData
86
+ api.post('/upload', { name: 'test', avatar: file }, { contentType: 'file' });
87
+
88
+ // 自定义 Content-Type(不转换数据)
89
+ api.post('/data', body, { contentType: 'text/plain' });
90
+ ```
91
+
92
+ **规则:**
93
+ - `contentType` 未设置 / `null` → 默认 `'json'`
94
+ - `headers` 中已有 `content-type` (大小写不敏感) → 跳过设置,但**仍会根据该 Content-Type 自动转换数据**
95
+ - `'json'` / 默认 / 自定义字符串 → `JSON.stringify(data)`(仅当 data 为对象时)
96
+ - `'file'` → `getFormData(data)` 转 FormData,不设置 Content-Type(浏览器自动带 boundary)
97
+ - `'form'` → `new URLSearchParams(data)` 转查询字符串
98
+ - 已是 FormData / URLSearchParams / 字符串 → 不做转换
99
+
100
+ ### 缓存破坏 (needCacheBust)
101
+
102
+ 所有请求默认自动在 params 中添加 `_` 参数(时间戳),防止浏览器或代理缓存。参数在 requestKey 生成之后添加,不影响防重复/取消请求的 key 匹配。
103
+
104
+ ```ts
105
+ // 默认开启,所有方法都加
106
+ api.get('/data'); // → /data?_=lq8x3f
107
+ api.post('/submit', { name: 'test' }); // → /submit?_=lq8x3f
108
+
109
+ // 关闭(实例级)
110
+ const api = createEnhanceInstance({ baseURL: '/api', needCacheBust: false });
111
+
112
+ // 关闭(请求级)
113
+ api.get('/data', null, { needCacheBust: false });
114
+ ```
115
+
116
+ ### 3. 失败重试 (retry)
117
+
118
+ **场景**:网络波动、5xx 错误、业务码异常时自动重试。
119
+
120
+ ```ts
121
+ api.get('/data', null, {
122
+ retry: {
123
+ retries: 3,
124
+ retryDelay: 1000,
125
+ exponential: true,
126
+ maxDelay: 30000,
127
+ }
128
+ });
129
+ ```
130
+
131
+ **默认重试条件**:网络错误(无 response)、408(超时)、429(限流)、5xx(服务器错误)
132
+
133
+ **2xx 业务码重试**:直接在 `retryCondition` 中判断:
134
+
135
+ ```ts
136
+ api.get('/data', null, {
137
+ retry: {
138
+ retries: 2,
139
+ retryCondition: (error) => {
140
+ // 2xx 但业务码异常 → 重试
141
+ if (error.response?.status === 200 && error.response?.data?.code !== 0) return true;
142
+ // 网络错误 / 5xx → 重试
143
+ if (!error.response || error.response.status >= 500) return true;
144
+ return false;
145
+ },
146
+ }
147
+ });
148
+ ```
149
+
150
+ ## 架构设计
151
+
152
+ ### 请求拦截器 (5 步)
153
+
154
+ ```
155
+ 请求拦截器
156
+
157
+ ├─ 步骤 1:获取有效配置(请求级 > 实例级)
158
+ ├─ 步骤 2:Content-Type 处理(默认 json,file 不设置)
159
+ ├─ 步骤 3:取消旧请求(同 cancelKey 的旧请求被中止)
160
+ ├─ 步骤 4:防重复检查(同 preventKey 且在 intervalMs 内则阻止)
161
+ ├─ 步骤 5:注册新请求(AbortController 注册到 requestManager)
162
+
163
+ └──→ 发送请求
164
+ ```
165
+
166
+ 防重复优先于取消请求:先执行步骤 3 取消 → 步骤 4 防重复检查 → 步骤 5 注册。
167
+
168
+ ### 响应拦截器
169
+
170
+ ```
171
+ 成功 (2xx):
172
+ ├─ 检测业务码重试(retryCondition 判断)
173
+ └─ resolve deferred → 清理 pendingReturns + requestManager
174
+
175
+ 错误 (非 2xx / 网络错误 / 取消):
176
+ 情况 1 — 防重复拦截:返回 deferred.promise(不清理)
177
+ 情况 2 — 请求被取消: reject deferred → 清理 → 抛出
178
+ 情况 3 — 满足重试条件:保留 deferred → 清理 requestManager → 延迟后重试
179
+ 情况 4 — 重试耗尽: reject deferred → 清理 → 抛出
180
+ ```
181
+
182
+ ### pendingReturns vs requestManager
183
+
184
+ ```
185
+ pendingReturns: Map<string, PendingDeferred>
186
+ ─ 存储 deferred(promise + resolve/reject 回调)
187
+ ─ 请求 A 创建 deferred → 请求 B 被阻止时拿到 A 的 deferred.promise
188
+ ─ 重试链复用同一个 deferred,等待者拿到最终结果
189
+
190
+ requestManager: RequestManager 实例
191
+ ─ preventPending: Map<string, PendingRequest> — 防重复注册
192
+ ─ cancelPending: Map<string, PendingRequest> — 取消注册
193
+ ─ 每个 PendingRequest: { key, config, controller, promise, timestamp }
194
+ ─ controller 用于 abort(),promise 指向 pendingReturns 中的 deferred.promise
195
+ ```
196
+
197
+ ### Deferred 机制
198
+
199
+ ```
200
+ 请求 A (POST)
201
+ ├─ 创建 deferred
202
+ ├─ pendingReturns.set(key, deferred)
203
+ ├─ A 完成 → deferred.resolve(response) → 通知等待者
204
+ └─ A 重试 → 保留 deferred,重试链复用
205
+
206
+ 请求 B (POST, 同 key)
207
+ ├─ 检测到 pending
208
+ ├─ 阻止当前请求
209
+ └─ 返回 deferred.promise → B 等待 A 的结果
210
+ ```
211
+
212
+ ### 取消机制
213
+
214
+ 使用 `AbortController` / `AbortSignal`(axios 1.x):
215
+
216
+ - `new AbortController()` 创建控制器
217
+ - `config.signal = controller.signal` 绑定
218
+ - `controller.abort(reason)` 取消
219
+ - `axios.isCancel(error)` 判断
220
+
221
+ ## 完整配置参考
222
+
223
+ ```ts
224
+ const api = createEnhanceInstance({
225
+ baseURL: '/api',
226
+ timeout: 10000,
227
+
228
+ // 缓存破坏(默认开启,所有方法自动加 _ 参数)
229
+ needCacheBust: true,
230
+
231
+ preventDuplicate: {
232
+ enabled: true,
233
+ methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
234
+ intervalMs: 1000,
235
+ },
236
+
237
+ cancelRequest: {
238
+ enabled: true,
239
+ methods: ['GET'],
240
+ },
241
+
242
+ retry: {
243
+ enabled: true,
244
+ retries: 3,
245
+ retryDelay: 1000,
246
+ exponential: true,
247
+ maxDelay: 30000,
248
+ retryCondition: (error) => {
249
+ if (!error.response) return true;
250
+ const status = error.response.status;
251
+ if (status === 408 || status === 429) return true;
252
+ if (status >= 500 && status < 600) return true;
253
+ return false;
254
+ },
255
+ },
256
+ });
257
+ ```
258
+
259
+ > 未提供 `requestKey` 时自动根据 method/url/params/data 生成 hash 作为 key。
260
+
261
+ ## 配置简写
262
+
263
+ | 写法 | 效果 |
264
+ |------|------|
265
+ | `preventDuplicate: true` | 启用 |
266
+ | `preventDuplicate: false` | 关闭 |
267
+ | `preventDuplicate: 2000` | 设置 `intervalMs` |
268
+ | `preventDuplicate: '${url}'` | 设置 `requestKey` 模板(结果自动 hash) |
269
+ | `preventDuplicate: (cfg, hash) => key` | 设置 `requestKey` 生成函数 |
270
+ | `preventDuplicate: ['GET', 'POST']` | 设置 `methods` |
271
+ | `cancelRequest: true` / `false` | 启用/关闭 |
272
+ | `cancelRequest: '${url}'` | 设置 `requestKey` 模板 |
273
+ | `cancelRequest: (cfg, hash) => key` | 设置 `requestKey` 生成函数 |
274
+ | `cancelRequest: ['GET']` | 设置 `methods` |
275
+ | `retry: true` / `false` | 启用/关闭 |
276
+ | `retry: 5` | 设置 `retries` |
277
+ | `retry: [408, 429, 500]` | 生成 retryCondition(匹配数组中状态码或网络错误) |
278
+ | `retry: (err) => condition` | 设置 `retryCondition` |
279
+
280
+ > 非 `false` 的快捷方式暗含 `enabled: true`。`methods: undefined` / `null` = 所有方法,`methods: []` = 不应用。
281
+
282
+ ## requestKey 模板
283
+
284
+ | 占位符 | 说明 | 示例值 |
285
+ |--------|------|--------|
286
+ | `${method}` | HTTP 方法 | `POST` |
287
+ | `${url}` | 请求路径 | `/api/submit` |
288
+ | `${params.xxx}` | 查询参数 | `{userId}` → `123` |
289
+ | `${data.xxx}` | 请求体属性 | `{userId}` → `123` |
290
+
291
+ 支持括号索引:`${data.users[0].name}`
292
+
293
+ > 模板解析后的明文结果会经过 `hash()` 处理再作为 key 使用。直接传入的静态字符串不做 hash。
294
+
295
+ ## enhance API
296
+
297
+ ```ts
298
+ const api = createEnhanceInstance({ baseURL: '/api' });
299
+
300
+ // 取消指定请求(返回 boolean 表示是否成功取消)
301
+ const cancelled = api.enhance.cancelRequest('search-query');
302
+
303
+ // 清空所有待处理请求(reject 所有等待中的 deferred)
304
+ api.enhance.clearAll();
305
+
306
+ // 查看请求状态
307
+ const status = api.enhance.getRequestStatus('submit-form');
308
+ // → { key, config, controller, promise, timestamp } | undefined
309
+
310
+ // 底层 RequestManager
311
+ api.enhance.requestManager.getPendingCount();
312
+ api.enhance.requestManager.getPendingKeys();
313
+ ```
314
+
315
+ ## 导出
316
+
317
+ ```ts
318
+ import {
319
+ createEnhanceInstance,
320
+ defaultRetryCondition,
321
+ getFormData,
322
+ hash,
323
+ version,
324
+ } from 'enhance-axios';
325
+
326
+ import type {
327
+ CreateEnhanceOptions, EnhanceInstance,
328
+ PreventDuplicateConfig, CancelRequestConfig, RetryConfig,
329
+ PreventDuplicateOption, CancelRequestOption, RetryOption,
330
+ ContentType,
331
+ } from 'enhance-axios';
332
+ ```
333
+
334
+ ## getFormData
335
+
336
+ ```ts
337
+ import { getFormData } from 'enhance-axios';
338
+
339
+ // File / Blob → 默认字段名 'file'
340
+ getFormData(file);
341
+ getFormData(blob);
342
+
343
+ // 自定义字段名
344
+ getFormData(file, 'avatar');
345
+
346
+ // FileList → 遍历
347
+ getFormData(fileInput.files);
348
+
349
+ // 数组
350
+ getFormData([file, 'text', 42]);
351
+
352
+ // 对象 → key 作为字段名
353
+ getFormData({ name: 'test', age: 18 });
354
+ // → { name: 'test', age: '18' }
355
+
356
+ // 嵌套 File
357
+ getFormData({ username: 'john', avatar: file });
358
+ // → { username: 'john', avatar: <File> }
359
+
360
+ // 嵌套对象用 . 连接
361
+ getFormData({ user: { name: 'test', email: 'a@b.com' } });
362
+ // → { 'user.name': 'test', 'user.email': 'a@b.com' }
363
+ ```
364
+
365
+ ## 示例项目
366
+
367
+ ```bash
368
+ cd example && node server.js
369
+ # 打开 http://localhost:3000
370
+ ```
371
+
372
+ | 按钮 | 说明 |
373
+ |------|------|
374
+ | 防重复 | 连续 5 个 POST,只有第 1 个真实发送 |
375
+ | 取消请求 | 连续 5 个 GET,只有最后 1 个完成 |
376
+ | 随机 500 | 50% 概率 500,验证自动重试 |
377
+ | 固定 500 | 固定 500,验证重试耗尽 |
378
+ | 网络错误 | 服务端断开连接,验证自动重试 |
379
+ | 业务码 | HTTP 200 + 业务码异常,验证重试 |
380
+ | 综合 | 同时测试防重复 + 取消 + 重试 |
381
+ | 单次 | 禁用增强的普通请求 |
382
+ | 缓存破坏 | 验证所有请求自动追加 _ 参数 |
383
+ | 数据转换 | 验证 json/form/file 自动转换数据格式 |
384
+
385
+ Mock 接口:`/api/submit`、`/api/search`、`/api/data`、`/api/echo`、`/api/error`、`/api/network-error`、`/api/business-error`、`/api/success`、`/api/users`
386
+
387
+ ## 问题排查
388
+
389
+ 遇到问题时,提交至 [GitHub Issues](https://github.com/anomalyco/enhance-axios/issues),附上:
390
+
391
+ - axios / enhance-axios 版本、运行环境、复现代码、错误信息
392
+
393
+ ## License
394
+
395
+ MIT