@huangqi_an/request 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +580 -2
- package/dist/adapters/axios.d.ts +11 -0
- package/dist/adapters/axios.js +56 -0
- package/dist/adapters/axios.js.map +1 -0
- package/dist/adapters/fetch.d.ts +12 -0
- package/dist/adapters/fetch.js +109 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/uniapp.d.ts +13 -0
- package/dist/adapters/uniapp.js +92 -0
- package/dist/adapters/uniapp.js.map +1 -0
- package/dist/chunk-FY6YU3PV.js +23 -0
- package/dist/chunk-FY6YU3PV.js.map +1 -0
- package/dist/chunk-GMJTG6KM.js +156 -0
- package/dist/chunk-GMJTG6KM.js.map +1 -0
- package/dist/chunk-KLAI63DX.js +26 -0
- package/dist/chunk-KLAI63DX.js.map +1 -0
- package/dist/chunk-OV6LXQTN.js +37 -0
- package/dist/chunk-OV6LXQTN.js.map +1 -0
- package/dist/chunk-UCTOFBDY.js +27 -0
- package/dist/chunk-UCTOFBDY.js.map +1 -0
- package/dist/chunk-Z4N22SLG.js +28 -0
- package/dist/chunk-Z4N22SLG.js.map +1 -0
- package/dist/core.d.ts +61 -0
- package/dist/core.js +12 -0
- package/dist/core.js.map +1 -0
- package/dist/enhancers/dedupe.d.ts +29 -0
- package/dist/enhancers/dedupe.js +37 -0
- package/dist/enhancers/dedupe.js.map +1 -0
- package/dist/enhancers/retry.d.ts +38 -0
- package/dist/enhancers/retry.js +47 -0
- package/dist/enhancers/retry.js.map +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +11 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/interceptors/auth.d.ts +17 -0
- package/dist/interceptors/auth.js +7 -0
- package/dist/interceptors/auth.js.map +1 -0
- package/dist/interceptors/index.d.ts +5 -0
- package/dist/interceptors/index.js +20 -0
- package/dist/interceptors/index.js.map +1 -0
- package/dist/interceptors/logger.d.ts +25 -0
- package/dist/interceptors/logger.js +7 -0
- package/dist/interceptors/logger.js.map +1 -0
- package/dist/interceptors/request-id.d.ts +23 -0
- package/dist/interceptors/request-id.js +7 -0
- package/dist/interceptors/request-id.js.map +1 -0
- package/dist/interceptors/unwrap.d.ts +22 -0
- package/dist/interceptors/unwrap.js +8 -0
- package/dist/interceptors/unwrap.js.map +1 -0
- package/dist/types-DSmcNU2O.d.ts +146 -0
- package/package.json +109 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 黄其安 (huangqi-an)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,581 @@
|
|
|
1
|
-
#
|
|
1
|
+
# request
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
统一请求层:一份业务代码,axios / fetch / uni-app 三端通用,也欢迎任何自定义通道。
|
|
4
|
+
|
|
5
|
+
- **零依赖核心**:`src/index.ts` 不带任何平台依赖;适配器、拦截器、装饰器都是按需导入的插件,不被 import 的模块不进 bundle
|
|
6
|
+
- **统一语义**:状态码判断、错误四分类、取消、超时、进度、拦截器——三端行为完全一致
|
|
7
|
+
- **可扩展**:自定义适配器只需一个函数契约;事件(`RequestEvents`)与平台专有参数(`adapterOptions`)都有封闭的扩展点
|
|
8
|
+
|
|
9
|
+
> 适用形态:npm 包(`@huangqi_an/request`)或源码直引(拷贝 `src/`),零构建。
|
|
10
|
+
> 本文档描述当前实现,`npm run test` 的 86 个用例即文档行为快照。
|
|
11
|
+
|
|
12
|
+
## 目录
|
|
13
|
+
|
|
14
|
+
- [快速开始](#快速开始)
|
|
15
|
+
- [工作原理](#工作原理)
|
|
16
|
+
- [API 参考](#api-参考)
|
|
17
|
+
- [createRequest(options)](#createrequestoptions) / [实例方法](#实例方法) / [RequestConfig](#requestconfig) / [RawResponse](#rawresponse) / [RequestError](#requesterror)
|
|
18
|
+
- [拦截器](#拦截器)
|
|
19
|
+
- [三条链的语义](#三条链的语义) / [createAuthInterceptor](#createauthinterceptor) / [createRequestIdInterceptor](#createrequestidinterceptor) / [createUnwrapInterceptor](#createunwrapinterceptor) / [createLoggerInterceptors](#createloggerinterceptors)
|
|
20
|
+
- [适配器装饰器](#适配器装饰器)
|
|
21
|
+
- [组合:use 链](#组合use-链) / [withRetry](#withretry) / [withDedupe](#withdedupe) / [幂等性默认](#幂等性默认)
|
|
22
|
+
- [适配器](#适配器)
|
|
23
|
+
- [选型](#选型) / [能力矩阵](#能力矩阵) / [uni-app 平台选择](#uni-app-平台选择) / [自定义适配器](#自定义适配器) / [extend:附加平台方法](#extend附加平台方法)
|
|
24
|
+
- [进阶](#进阶)
|
|
25
|
+
- [统一取消](#统一取消) / [事件](#事件) / [adapterOptions 透传](#adapteroptions-透传)
|
|
26
|
+
- [设计决策](#设计决策)
|
|
27
|
+
- [开发](#开发)
|
|
28
|
+
|
|
29
|
+
## 快速开始
|
|
30
|
+
|
|
31
|
+
### 1. 安装
|
|
32
|
+
|
|
33
|
+
**npm(推荐)**:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @huangqi_an/request
|
|
37
|
+
# 仅使用 axiosAdapter 时需要自备 axios(optional peer dependency):
|
|
38
|
+
npm install axios
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**源码直引**:把 `src/` 拷进项目(或作为 monorepo 共享包引入),零构建;
|
|
42
|
+
本文示例中的 `@huangqi_an/request/...` 换成相对路径(如 `./src`)即可。
|
|
43
|
+
|
|
44
|
+
### 2. 创建实例并发请求(Web / Node / SSR)
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { createRequest } from './src'
|
|
48
|
+
import { fetchAdapter } from './src/adapters/fetch'
|
|
49
|
+
|
|
50
|
+
export const http = createRequest({
|
|
51
|
+
adapter: fetchAdapter, // 零依赖;Node 18+ / 现代浏览器
|
|
52
|
+
baseURL: 'https://api.example.com',
|
|
53
|
+
timeout: 15000,
|
|
54
|
+
headers: { 'X-Client': 'web' },
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// 发请求
|
|
58
|
+
const res = await http.get('/users', { params: { page: 1 } })
|
|
59
|
+
res.data // 响应体(JSON 自动解析,视 adapter 而定)
|
|
60
|
+
res.status // 200
|
|
61
|
+
res.headers // Record<string, string>
|
|
62
|
+
res.durationMs // 耗时(core 注入)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. uni-app
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createRequest } from './src'
|
|
69
|
+
import { uniappAdapter } from './src/adapters/uniapp'
|
|
70
|
+
|
|
71
|
+
export const http = createRequest({
|
|
72
|
+
adapter: uniappAdapter, // 基于 uni.request,H5 / 小程序 / App 通用
|
|
73
|
+
baseURL: 'https://api.example.com',
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. 统一错误处理
|
|
78
|
+
|
|
79
|
+
所有失败最终抛出同一个 `RequestError`,按 `kind` 四分类:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { RequestError } from './src'
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await http.post('/orders', { sku: 1 })
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err instanceof RequestError) {
|
|
88
|
+
switch (err.kind) {
|
|
89
|
+
case 'http': // 非 2xx:err.status / err.data / err.headers
|
|
90
|
+
break
|
|
91
|
+
case 'business': // HTTP 2xx 但业务码失败(unwrap 拦截器产生):err.data 为原始 body
|
|
92
|
+
break
|
|
93
|
+
case 'abort': // 请求被取消
|
|
94
|
+
break
|
|
95
|
+
case 'network': // 传输层失败(含超时):err.raw 保留原始错误可排查
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 工作原理
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
业务代码
|
|
106
|
+
│ http.get('/users')
|
|
107
|
+
▼
|
|
108
|
+
合并默认值(baseURL/headers/timeout)→ 请求链拦截器 → params 拼进 url → 取消检查
|
|
109
|
+
▼
|
|
110
|
+
adapter(fetch / axios / uniapp / 自定义)——任何状态码都 resolve 成 RawResponse
|
|
111
|
+
▼
|
|
112
|
+
core 判 2xx ──▶ 响应链拦截器(剥壳/转换)──▶ RawResponse ──▶ 业务代码
|
|
113
|
+
│
|
|
114
|
+
└ 非 2xx / 异常 ──▶ 错误链拦截器(可恢复)──▶ 抛 RequestError ──▶ 业务代码
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
三类插件各司其职:
|
|
118
|
+
|
|
119
|
+
| 插件目录 | 是什么 | 挂在哪 |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `src/interceptors/` | 三条链上的处理函数(auth / requestId / unwrap / logger) | `http.interceptors.*` |
|
|
122
|
+
| `src/enhancers/` | 包装 adapter 的装饰器(retry / dedupe) | `adapter.use()` |
|
|
123
|
+
| `src/adapters/` | 平台发送实现(fetch / axios / uniapp) | `createRequest({ adapter })` |
|
|
124
|
+
|
|
125
|
+
导入路径一览(门面 `src/index.ts` 只含契约,插件一律按需导入):
|
|
126
|
+
|
|
127
|
+
| 需要什么 | 从哪导入 |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `createRequest` / `defineAdapter` / 类型 / 错误 | `./src`(零依赖,任何端) |
|
|
130
|
+
| 适配器 | `./src/adapters/{fetch,axios,uniapp}` |
|
|
131
|
+
| 内置拦截器 | `./src/interceptors/{auth,request-id,unwrap,logger}` |
|
|
132
|
+
| 装饰器 | `./src/enhancers/{retry,dedupe}` |
|
|
133
|
+
|
|
134
|
+
## API 参考
|
|
135
|
+
|
|
136
|
+
### createRequest(options)
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { createRequest } from './src'
|
|
140
|
+
|
|
141
|
+
const http = createRequest({ adapter: fetchAdapter, baseURL: '...' })
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| 选项 | 类型 | 必填 | 说明 |
|
|
145
|
+
| --- | --- | --- | --- |
|
|
146
|
+
| `adapter` | `RequestAdapter` | ✓ | 平台适配器,或装饰器组合结果(`axiosAdapter.use(...)`) |
|
|
147
|
+
| `baseURL` | `string` | — | 相对 url 的前缀;绝对 url 自动忽略 |
|
|
148
|
+
| `headers` | `Record<string, string>` | — | 实例级默认头,请求级同名覆盖 |
|
|
149
|
+
| `timeout` | `number` | — | 毫秒;三端统一归类为 `kind: 'network'` |
|
|
150
|
+
|
|
151
|
+
### 实例方法
|
|
152
|
+
|
|
153
|
+
| 方法 | 说明 |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `request<T>(config)` | 全量配置发请求,`method` 接受任意 `RequestMethod`(含 PATCH 等)——这是唯一必需的入口 |
|
|
156
|
+
| `get<T>(url, config?)` | 快捷方法,`config` 中省略 url/method |
|
|
157
|
+
| `post<T>(url, data?, config?)` | 同上,省略 url/method/data |
|
|
158
|
+
| `put<T>(url, data?, config?)` | 同上 |
|
|
159
|
+
| `patch<T>(url, data?, config?)` | 同上 |
|
|
160
|
+
| `delete<T>(url, config?)` | 同上 |
|
|
161
|
+
| `http.interceptors.request.use(fn)` | 注册请求链拦截器 |
|
|
162
|
+
| `http.interceptors.response.use(fn)` | 注册响应链拦截器(成功转换) |
|
|
163
|
+
| `http.interceptors.error.use(fn)` | 注册错误链拦截器 |
|
|
164
|
+
|
|
165
|
+
全部请求方法返回 `Promise<RawResponse<T>>`。
|
|
166
|
+
|
|
167
|
+
快捷方法只是 `request()` 的语法糖,你不调用就没有任何成本。需要**裁剪方法面**(如团队规范禁用
|
|
168
|
+
DELETE)或自定义动词时,在业务层组合自己的 client 对象——库保持完整能力,导出面由你决定:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// src/api.ts —— 团队实际消费的入口
|
|
172
|
+
import { createRequest } from './src'
|
|
173
|
+
import { fetchAdapter } from './src/adapters/fetch'
|
|
174
|
+
|
|
175
|
+
const base = createRequest({ adapter: fetchAdapter, baseURL: 'https://api.example.com' })
|
|
176
|
+
|
|
177
|
+
export const api = {
|
|
178
|
+
get: base.get,
|
|
179
|
+
post: base.post,
|
|
180
|
+
patch: (url: string, data?: unknown) => base.request({ url, data, method: 'PATCH' }),
|
|
181
|
+
}
|
|
182
|
+
// 没有 put/delete:业务方拿不到,core 的任何语义不受影响
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### RequestConfig
|
|
186
|
+
|
|
187
|
+
| 字段 | 类型 | 说明 |
|
|
188
|
+
| --- | --- | --- |
|
|
189
|
+
| `url` | `string` | 必填;相对 url 拼 baseURL,绝对 url 直接使用 |
|
|
190
|
+
| `method` | `RequestMethod` | 默认 `'GET'` |
|
|
191
|
+
| `baseURL` | `string` | 请求级覆盖实例级 |
|
|
192
|
+
| `params` | `Record<string, unknown>` | query 参数,core 统一序列化(数组重复 key、跳过 null/undefined、encodeURIComponent) |
|
|
193
|
+
| `data` | `unknown` | 请求体;fetch 端对象/数组自动 JSON 序列化并补 `Content-Type` |
|
|
194
|
+
| `headers` | `Record<string, string>` | 与实例级浅合并,请求级优先 |
|
|
195
|
+
| `timeout` | `number` | 毫秒 |
|
|
196
|
+
| `signal` | `AbortSignal` | 统一取消(见[统一取消](#统一取消)) |
|
|
197
|
+
| `on` | `RequestEvents` | 事件订阅(见[事件](#事件)) |
|
|
198
|
+
| `adapterOptions` | `AdapterOptions` | 平台专有参数透传(见[adapterOptions 透传](#adapteroptions-透传)) |
|
|
199
|
+
|
|
200
|
+
### RawResponse
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
interface RawResponse<T = unknown> {
|
|
204
|
+
status: number // HTTP 状态码
|
|
205
|
+
data: T // 响应体
|
|
206
|
+
headers: Record<string, string>
|
|
207
|
+
durationMs?: number // core 注入:adapter 调用耗时
|
|
208
|
+
config?: RequestConfig // core 注入:最终请求配置(url 已拼好 params)
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`durationMs` / `config` 由 core 统一注入,三端形态一致;`res.config.headers` 可取到
|
|
213
|
+
requestId 拦截器注入的请求 ID(报障定位),响应链也可按 url 做条件逻辑。
|
|
214
|
+
|
|
215
|
+
### RequestError
|
|
216
|
+
|
|
217
|
+
| 字段 | 说明 |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `kind` | `'abort' \| 'http' \| 'business' \| 'network'`,见下表 |
|
|
220
|
+
| `status?` | HTTP 状态码(http / business 时存在) |
|
|
221
|
+
| `data?` | 响应体或原始 body |
|
|
222
|
+
| `headers?` | 响应头(http 时) |
|
|
223
|
+
| `config?` | 最终请求配置(可取请求 ID 等注入头) |
|
|
224
|
+
| `raw?` | 原始错误:axios 的 `error.code`(如 `ECONNABORTED` 超时)、uni 的 `errMsg` |
|
|
225
|
+
|
|
226
|
+
| kind | 含义 | 来源 |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| `http` | 收到响应但状态码非 2xx | core |
|
|
229
|
+
| `business` | HTTP 2xx 但业务码失败 | unwrap 拦截器 |
|
|
230
|
+
| `abort` | 请求被取消(AbortSignal / 平台取消) | 各适配器 |
|
|
231
|
+
| `network` | 传输层失败,含超时 | core 包装适配器抛错 |
|
|
232
|
+
|
|
233
|
+
## 拦截器
|
|
234
|
+
|
|
235
|
+
### 三条链的语义
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
// 请求链:按注册顺序执行,返回新 config(注入 token / 请求 ID 等)
|
|
239
|
+
http.interceptors.request.use((config) => ({
|
|
240
|
+
...config,
|
|
241
|
+
headers: { ...config.headers, Authorization: `Bearer ${getToken()}` },
|
|
242
|
+
}))
|
|
243
|
+
|
|
244
|
+
// 响应链(成功):转换 / 剥壳
|
|
245
|
+
http.interceptors.response.use((res) => ({ ...res, data: res.data.data }))
|
|
246
|
+
|
|
247
|
+
// 错误链:所有失败都会经过(可恢复 / 可替换 / 可观察)
|
|
248
|
+
http.interceptors.error.use((err) => {
|
|
249
|
+
report(err) // 无返回值 = 放行
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
规则:
|
|
254
|
+
|
|
255
|
+
1. 三条链相互独立,均**按注册顺序**执行
|
|
256
|
+
2. 响应链任一环节抛错,进入错误链
|
|
257
|
+
3. 错误链入口先归一化:所有失败(请求拦截器抛错 / 网络失败 / 非 2xx / 响应链抛错)都是 `RequestError`
|
|
258
|
+
4. 错误链行为:**返回 `RawResponse` = 恢复**(结束错误链,直接作为最终结果);**返回/抛出 `RequestError` = 替换**当前错误继续走链;**无返回值 = 放行**(纯观察)
|
|
259
|
+
5. 要"所有错误都上报",上报拦截器必须**最先注册**——恢复发生后就不再通知后续拦截器
|
|
260
|
+
|
|
261
|
+
### createAuthInterceptor
|
|
262
|
+
|
|
263
|
+
请求链:从 `getToken()` 注入凭证头,支持异步(如先静默刷新)。
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { createAuthInterceptor } from './src/interceptors'
|
|
267
|
+
|
|
268
|
+
http.interceptors.request.use(
|
|
269
|
+
createAuthInterceptor({
|
|
270
|
+
getToken: () => getToken(),
|
|
271
|
+
shouldAttach: (config) => !config.url.includes('/login'),
|
|
272
|
+
}),
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
| 选项 | 默认 | 说明 |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| `getToken` | 必传 | 返回 token(可异步);返回空值则跳过注入 |
|
|
279
|
+
| `headerName` | `'Authorization'` | 注入的头名 |
|
|
280
|
+
| `scheme` | `'Bearer '` | token 前缀;不需要传空字符串 |
|
|
281
|
+
| `shouldAttach(config)` | — | 返回 false 的请求不注入 |
|
|
282
|
+
|
|
283
|
+
### createRequestIdInterceptor
|
|
284
|
+
|
|
285
|
+
请求链:每个请求生成唯一 ID 注入请求头,用于前后端日志关联与报障定位。
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { createRequestIdInterceptor } from './src/interceptors'
|
|
289
|
+
|
|
290
|
+
http.interceptors.request.use(createRequestIdInterceptor())
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- core 会把最终 config 盖章到 `RawResponse.config` 与 `RequestError.config`,响应日志、错误上报都能取到同一个 ID
|
|
294
|
+
- 重试共享同一 config:同一次逻辑请求(含重试)带同一个 ID,服务端可识别重发
|
|
295
|
+
|
|
296
|
+
| 选项 | 默认 | 说明 |
|
|
297
|
+
| --- | --- | --- |
|
|
298
|
+
| `headerName` | `'X-Request-Id'` | 请求头名 |
|
|
299
|
+
| `generateId()` | `crypto.randomUUID()` | 运行时缺失时退化为时间戳+随机数;可接入 tracing/Sentry 等链路库 |
|
|
300
|
+
| `keepExisting` | `false` | 已带该头时不覆盖(上游链路透传)。注意:实例级默认头也算"已携带",开启会导致所有请求共用一个 ID,慎用 |
|
|
301
|
+
|
|
302
|
+
### createUnwrapInterceptor
|
|
303
|
+
|
|
304
|
+
响应链:剥离 `{code, data, message}` 业务壳;业务码失败抛 `kind: 'business'` 的 `RequestError`(`data` 为原始 body,进入错误链)。
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { createUnwrapInterceptor } from './src/interceptors'
|
|
308
|
+
|
|
309
|
+
http.interceptors.response.use(createUnwrapInterceptor())
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| 选项 | 默认 | 说明 |
|
|
313
|
+
| --- | --- | --- |
|
|
314
|
+
| `successCodes` | `[0]` | 业务成功码,允许字符串码 |
|
|
315
|
+
| `codeKey` / `dataKey` / `messageKey` | `'code' / 'data' / 'message'` | 字段名 |
|
|
316
|
+
| — | — | body 非对象或缺业务码字段时原样放行(防御非 JSON 响应) |
|
|
317
|
+
|
|
318
|
+
### createLoggerInterceptors
|
|
319
|
+
|
|
320
|
+
三链日志:返回 `{ request, response, error }` 三件套,分别注册。
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { createLoggerInterceptors } from './src/interceptors'
|
|
324
|
+
|
|
325
|
+
const logger = createLoggerInterceptors() // 默认 console
|
|
326
|
+
http.interceptors.request.use(logger.request)
|
|
327
|
+
http.interceptors.response.use(logger.response)
|
|
328
|
+
http.interceptors.error.use(logger.error)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- request:method / url / params;response:status / 耗时 / url;error:kind / message / url(warn 级别)
|
|
332
|
+
- 选项 `log(level, ...args)` 可替换为埋点 SDK;注入的函数**不应抛错**
|
|
333
|
+
|
|
334
|
+
## 适配器装饰器
|
|
335
|
+
|
|
336
|
+
### 组合:use 链
|
|
337
|
+
|
|
338
|
+
适配器经 `defineAdapter` 导出,本身可直接用,也可 `.use()` 叠加装饰器。
|
|
339
|
+
**注册顺序 = 请求流经顺序**(与拦截器链一致):`.use(withDedupe()).use(withRetry())`
|
|
340
|
+
从上往下读就是请求经过的顺序——先去重(合并并发),再去重窗口内重试。
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { axiosAdapter } from './src/adapters/axios'
|
|
344
|
+
import { withRetry } from './src/enhancers/retry'
|
|
345
|
+
import { withDedupe } from './src/enhancers/dedupe'
|
|
346
|
+
|
|
347
|
+
const adapter = axiosAdapter.use(withDedupe()).use(withRetry({ retries: 2 }))
|
|
348
|
+
const http = createRequest({ adapter })
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`.use` 返回新适配器、不修改原对象,分支链互不影响。
|
|
352
|
+
|
|
353
|
+
### withRetry
|
|
354
|
+
|
|
355
|
+
失败自动重试。与错误链恢复不同:重试成功的响应会正常走完整管道(响应链剥壳等)。
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { withRetry } from './src/enhancers/retry'
|
|
359
|
+
|
|
360
|
+
const adapter = fetchAdapter.use(withRetry({ retries: 2, delayMs: (n) => n * 300 }))
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
| 选项 | 默认 | 说明 |
|
|
364
|
+
| --- | --- | --- |
|
|
365
|
+
| `retries` | `2` | 最大重试次数(不含首次;总尝试 = 1 + retries) |
|
|
366
|
+
| `methods` | `['GET', 'HEAD']` | 默认重试规则覆盖的方法 |
|
|
367
|
+
| `shouldRetry({error?, response?}, attempt, config)` | — | 提供后**完全接管**默认规则(`methods` 不再生效) |
|
|
368
|
+
| `delayMs(attempt)` | 不等待 | 第 attempt 次重试前的等待毫秒 |
|
|
369
|
+
|
|
370
|
+
默认规则:抛错且方法幂等 → 重试;非 2xx 响应、取消 → 不重试(取消无论配置如何都不重试)。
|
|
371
|
+
|
|
372
|
+
### withDedupe
|
|
373
|
+
|
|
374
|
+
进行中的相同请求共享同一个 Promise(settle 后自动清理)。默认只对幂等方法生效。
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
import { withDedupe } from './src/enhancers/dedupe'
|
|
378
|
+
|
|
379
|
+
const adapter = fetchAdapter.use(withDedupe()) // GET 去重
|
|
380
|
+
const antiDoubleSubmit = fetchAdapter.use(withDedupe({ methods: ['POST'] })) // 防双击
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| 选项 | 默认 | 说明 |
|
|
384
|
+
| --- | --- | --- |
|
|
385
|
+
| `methods` | `['GET', 'HEAD']` | 参与去重的方法;写方法默认放行 |
|
|
386
|
+
| `key(config)` | `` `${method} ${url} ${JSON.stringify(data)}` `` | params 已拼进 url;data 无法序列化时该请求不参与去重(宁可漏,不可错合并) |
|
|
387
|
+
|
|
388
|
+
### 幂等性默认
|
|
389
|
+
|
|
390
|
+
两个装饰器都只对幂等方法(GET/HEAD)默认生效——
|
|
391
|
+
|
|
392
|
+
- dedupe 的 share 语义不会"多发",但会"少发":对故意并发的写请求(如计数 +1)静默合并等于丢操作
|
|
393
|
+
- retry 对写方法自动重发,可能重复执行已到达服务端的请求(如重复扣款)
|
|
394
|
+
|
|
395
|
+
所以写方法默认放行 / 不重试;需要时显式 opt-in(dedupe 传 `methods: ['POST']` 做防双击、retry 提供自定义 `shouldRetry`)。资金类接口请以后端幂等键(Idempotency-Key)兜底,前端只是第一道闸。
|
|
396
|
+
|
|
397
|
+
## 适配器
|
|
398
|
+
|
|
399
|
+
### 选型
|
|
400
|
+
|
|
401
|
+
| 适配器 | 依赖 | 适用 |
|
|
402
|
+
| --- | --- | --- |
|
|
403
|
+
| `fetchAdapter` | 无 | Web / Node 18+ / SSR 的默认选择 |
|
|
404
|
+
| `axiosAdapter` | axios | 需要 axios 生态(拦截器插件、老浏览器兼容、`adapterOptions.axios.adapter` 切换传输)时 |
|
|
405
|
+
| `uniappAdapter` | 全局 `uni` | uni-app 项目(H5 / 小程序 / App) |
|
|
406
|
+
| 自定义 | — | wx 原生小程序、Taro、Electron net……按契约接入(见下) |
|
|
407
|
+
|
|
408
|
+
### 能力矩阵
|
|
409
|
+
|
|
410
|
+
平台差异在适配器内抹平,做不到的静默不触发;超时与取消三端行为一致。
|
|
411
|
+
|
|
412
|
+
| 适配器 | 上传进度 | 下载进度 | 说明 |
|
|
413
|
+
| --- | --- | --- | --- |
|
|
414
|
+
| `axiosAdapter` | ✓ | ✓ | axios 1.x 浏览器 XHR 与 Node 流式适配器均支持 |
|
|
415
|
+
| `uniappAdapter` | ✓ | ✓ | `task.onProgressUpdate` 归一化(旧基础库缺失该 API 时跳过) |
|
|
416
|
+
| `fetchAdapter` | ✗ | ✓ | fetch 无上传进度(平台限制);下载走 `res.body` 流式读取,`Content-Length` 缺失时 `total/progress` 为 undefined |
|
|
417
|
+
|
|
418
|
+
> HTTP 动词备注:`method` 直接透传(`RequestMethod` 含全动词),但微信小程序端受 `wx.request`
|
|
419
|
+
> 方法白名单限制(仅 OPTIONS/GET/HEAD/POST/PUT/DELETE/TRACE/CONNECT,**不含 PATCH**)——
|
|
420
|
+
> mp 端需要 PATCH 时用 POST + 语义路径代替,或经 `adapterOptions: { uniapp: {...} }` 定制。
|
|
421
|
+
|
|
422
|
+
### uni-app 平台选择
|
|
423
|
+
|
|
424
|
+
注意:**uni-app ≠ 原生微信小程序**。本库的 `uniappAdapter` 依赖 uni-app 全局 `uni.request`;
|
|
425
|
+
原生小程序用的是 `wx.request`,那是另一套 API,需要单独的适配器(按契约实现即可,见下)。
|
|
426
|
+
uni-app 项目用条件编译分别创建实例(导入路径按你放置 `src/` 的位置调整):
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
// #ifdef H5
|
|
430
|
+
import { fetchAdapter } from '@/request/adapters/fetch'
|
|
431
|
+
export const http = createRequest({ adapter: fetchAdapter })
|
|
432
|
+
// #endif
|
|
433
|
+
// #ifdef MP-WEIXIN
|
|
434
|
+
import { uniappAdapter } from '@/request/adapters/uniapp'
|
|
435
|
+
export const http = createRequest({ adapter: uniappAdapter })
|
|
436
|
+
// #endif
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### 自定义适配器
|
|
440
|
+
|
|
441
|
+
契约只有三句话:
|
|
442
|
+
|
|
443
|
+
1. 输入 `RequestConfig`——其中的 `url` 已由 core 拼好 params,是完整地址
|
|
444
|
+
2. 返回 `RawResponse`(`status` / `data` / `headers`),**任何状态码都 resolve,判断交给 core**
|
|
445
|
+
3. 取消时抛 `name='AbortError'` 的错误,其余错误随意抛(core 统一包装为 `kind: 'network'`,`raw` 保留原始错误)
|
|
446
|
+
|
|
447
|
+
`defineAdapter` 把实现函数包装为可链式增强的适配器(参数获得类型推断):
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
import { createRequest, defineAdapter } from './src'
|
|
451
|
+
|
|
452
|
+
const myAdapter = defineAdapter(async (config) => {
|
|
453
|
+
const res = await myTransport.request(config.url, config)
|
|
454
|
+
return { status: res.status, data: res.body, headers: res.headers }
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
const http = createRequest({ adapter: myAdapter.use(withRetry()) })
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
可运行的完整示例见 `tests/custom-adapter.test.ts`(一个与内置 `adapters/fetch.ts` 同构的 fetch 实现)。
|
|
461
|
+
|
|
462
|
+
### extend:附加平台方法
|
|
463
|
+
|
|
464
|
+
适配器可携带平台特有方法(如小程序的 `uploadFile`)。`.extend` 的工厂收到**组合后的适配器**,
|
|
465
|
+
附加方法可路由完整管道(此前的拦截器/装饰器都生效),并在后续 `.use` 链上保持不丢:
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
import { uniappAdapter } from './src/adapters/uniapp'
|
|
469
|
+
import { withRetry } from './src/enhancers/retry'
|
|
470
|
+
|
|
471
|
+
const mp = uniappAdapter
|
|
472
|
+
.use(withRetry({ methods: ['GET', 'POST'] }))
|
|
473
|
+
.extend((request) => ({
|
|
474
|
+
upload: (filePath: string, url: string) =>
|
|
475
|
+
new Promise((resolve, reject) => {
|
|
476
|
+
uni.uploadFile({
|
|
477
|
+
url,
|
|
478
|
+
filePath,
|
|
479
|
+
name: 'file',
|
|
480
|
+
success: (res) => resolve({ status: res.statusCode, data: res.data, headers: {} }),
|
|
481
|
+
fail: reject,
|
|
482
|
+
})
|
|
483
|
+
}),
|
|
484
|
+
}))
|
|
485
|
+
|
|
486
|
+
await mp.post('/x', {}) // 统一面不变
|
|
487
|
+
await mp.upload('/a.png', uploadUrl) // 平台特有方法(已带 retry)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
语义:工厂以 `extend` 调用时刻的组合执行一次——要让附加方法也享受某个装饰器,**先 `.use` 再
|
|
491
|
+
`.extend`**;之后对同一对象的 `.use` 不影响已附加方法。自定义适配器同样支持
|
|
492
|
+
(`defineAdapter(fn).extend(...)`)。附加方法不要命名为 `use` / `extend`。
|
|
493
|
+
|
|
494
|
+
## 进阶
|
|
495
|
+
|
|
496
|
+
### 统一取消
|
|
497
|
+
|
|
498
|
+
所有适配器都走标准 `AbortSignal`:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
const controller = new AbortController()
|
|
502
|
+
http.get('/heavy-report', { signal: controller.signal }).catch(() => {})
|
|
503
|
+
controller.abort() // axios:原生取消;uni.request:桥接到 task.abort();fetch:原生取消
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 事件
|
|
507
|
+
|
|
508
|
+
事件通过 `config.on` 订阅,进度事件统一为 `{ loaded, total?, progress? }`(`progress` 为 0-1,`total` 未知时为 undefined):
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
await http.post('/upload', formData, {
|
|
512
|
+
on: {
|
|
513
|
+
uploadProgress: ({ progress }) => bar.update(progress),
|
|
514
|
+
},
|
|
515
|
+
})
|
|
516
|
+
await http.get('/big-file', {
|
|
517
|
+
on: {
|
|
518
|
+
downloadProgress: ({ loaded, total }) => console.log(loaded, total),
|
|
519
|
+
},
|
|
520
|
+
})
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**扩展策略**:`on` 是事件的唯一入口——以后新增事件(如 `headersReceived`、`chunkReceived`、`retry`)
|
|
524
|
+
只扩展 `RequestEvents` 接口并在对应适配器中 emit,`RequestConfig` 结构保持封闭。
|
|
525
|
+
|
|
526
|
+
### adapterOptions 透传
|
|
527
|
+
|
|
528
|
+
平台特有参数不进 `RequestConfig` 根,统一走 `adapterOptions`(按适配器名取用、仅对应适配器消费):
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
// 让 axios 走它自己的 fetch 适配器(axios 1.7+)
|
|
532
|
+
await http.get('/x', { adapterOptions: { axios: { adapter: 'fetch' } } })
|
|
533
|
+
|
|
534
|
+
// fetch 携带 cookie / 控制缓存
|
|
535
|
+
await http.get('/me', { adapterOptions: { fetch: { credentials: 'include', cache: 'no-store' } } })
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
冲突规则:`url / method / headers / timeout / signal / validateStatus` 等契约派生字段始终以
|
|
539
|
+
`RequestConfig` 为准,透传项覆盖不了它们——统一语义不被破坏。
|
|
540
|
+
|
|
541
|
+
实例级默认透传用请求拦截器即可,不需要额外 API:
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
http.interceptors.request.use((config) => ({
|
|
545
|
+
...config,
|
|
546
|
+
adapterOptions: { ...config.adapterOptions, axios: { adapter: 'fetch' } },
|
|
547
|
+
}))
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## 设计决策
|
|
551
|
+
|
|
552
|
+
| 决策 | 原因 |
|
|
553
|
+
| --- | --- |
|
|
554
|
+
| 主入口零依赖,插件按需导入 | 依赖各自携带,未被 import 的模块不进 bundle;小程序包体积与平台耦合由结构保证,而非约定 |
|
|
555
|
+
| 适配器只返回 `RawResponse`,不做任何判断 | 404/500 在两端表现不同(axios 抛错、uni 走 success),统一 resolve 交给 core 判 2xx,是三端语义归一的支点 |
|
|
556
|
+
| params 在 core 拼成完整 URL | 三端行为完全一致(数组重复 key、跳过 null/undefined、encode 规则),适配器保持"哑" |
|
|
557
|
+
| 取消统一为 `name='AbortError'` 的 Error | uni-app 非 H5 运行时(小程序/App 端)没有 `DOMException`;`e.name === 'AbortError'` 的判定方式各端兼容 |
|
|
558
|
+
| core dispatch 前先查 `signal.aborted` | 防止"已取消还发请求"的竞态 |
|
|
559
|
+
| 错误独立成链,undefined 即放行 | 观察(上报/toast)与恢复/替换同一入口;观察类钩子不必被迫 rethrow |
|
|
560
|
+
| 事件走 `RequestEvents`,`RequestConfig` 结构封闭 | 新增事件只扩接口并在适配器 emit,契约不再膨胀 |
|
|
561
|
+
| `adapterOptions` 逃生舱,契约字段优先 | 平台特有参数有统一去处,且统一语义(状态码/取消/超时)不可被透传破坏 |
|
|
562
|
+
| retry / dedupe 默认仅幂等方法 | 写请求宁可放行(不去重、不重试),绝不静默吞操作或重发非幂等操作 |
|
|
563
|
+
| 错误带 `raw` 字段 | 保留 axios 的 `error.code`(如 `ECONNABORTED` 超时)和 uni 的 `errMsg`,排查不丢信息 |
|
|
564
|
+
|
|
565
|
+
## 开发
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
npm install
|
|
569
|
+
npm run test # vitest,86 个用例
|
|
570
|
+
npm run typecheck # tsc --noEmit
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
| 测试文件 | 覆盖 |
|
|
574
|
+
| --- | --- |
|
|
575
|
+
| `tests/core.test.ts` | mock 适配器:配置合并、拦截器顺序、params 拼接、错误四分类、错误链恢复/替换/放行 |
|
|
576
|
+
| `tests/adapter.fetch.test.ts` | 真实 fetch 打本地 server:形状、404、body 序列化、进度(含 chunked)、取消、超时、透传 |
|
|
577
|
+
| `tests/adapter.axios.test.ts` | 真实 axios 打本地 server:形状、404、进度、`adapter: 'fetch'` 切换、透传冲突规则 |
|
|
578
|
+
| `tests/adapter.uniapp.test.ts` | fake `globalThis.uni`:success/fail 映射、500 透传、取消、进度归一化、透传 |
|
|
579
|
+
| `tests/interceptors.test.ts` | auth / unwrap / logger / requestId 全选项覆盖 |
|
|
580
|
+
| `tests/enhancers.test.ts` | retry/dedupe 全场景(含幂等默认)+ `.use` 链式语义 |
|
|
581
|
+
| `tests/custom-adapter.test.ts` | 第三方按契约接入的可行性证明 |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ChainableAdapter } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Web 端 adapter 插件:基于 axios。按需导入,主入口不含本模块。
|
|
5
|
+
* 经 defineAdapter 导出:可直接使用,也可 .use() 叠加装饰器(withRetry / withDedupe 等)。
|
|
6
|
+
* - validateStatus: () => true:任何状态码都 resolve,404/500 等判断统一交给 core
|
|
7
|
+
* - axios 原生支持 AbortSignal,取消语义在此翻译为统一的 AbortError
|
|
8
|
+
*/
|
|
9
|
+
declare const axiosAdapter: ChainableAdapter;
|
|
10
|
+
|
|
11
|
+
export { axiosAdapter };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineAdapter
|
|
3
|
+
} from "../chunk-GMJTG6KM.js";
|
|
4
|
+
import {
|
|
5
|
+
createAbortError
|
|
6
|
+
} from "../chunk-Z4N22SLG.js";
|
|
7
|
+
|
|
8
|
+
// src/adapters/axios.ts
|
|
9
|
+
import axios from "axios";
|
|
10
|
+
function toProgress(loaded, total) {
|
|
11
|
+
const t = total && total > 0 ? total : void 0;
|
|
12
|
+
return {
|
|
13
|
+
loaded,
|
|
14
|
+
total: t,
|
|
15
|
+
progress: t !== void 0 ? Math.min(loaded / t, 1) : void 0
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var axiosAdapter = defineAdapter(async (config) => {
|
|
19
|
+
const { uploadProgress, downloadProgress } = config.on ?? {};
|
|
20
|
+
try {
|
|
21
|
+
const res = await axios.request({
|
|
22
|
+
// 专有配置先展开(如 adapter: 'fetch'):契约派生字段在后覆盖,
|
|
23
|
+
// url/method/headers/timeout/signal/validateStatus 始终以 RequestConfig 为准
|
|
24
|
+
...config.adapterOptions?.axios,
|
|
25
|
+
url: config.url,
|
|
26
|
+
method: config.method ?? "GET",
|
|
27
|
+
data: config.data,
|
|
28
|
+
headers: config.headers,
|
|
29
|
+
timeout: config.timeout,
|
|
30
|
+
signal: config.signal,
|
|
31
|
+
validateStatus: () => true,
|
|
32
|
+
// axios 1.x 浏览器(XHR)与 Node(流式)适配器均产生进度事件
|
|
33
|
+
...uploadProgress && {
|
|
34
|
+
onUploadProgress: (e) => uploadProgress(toProgress(e.loaded, e.total))
|
|
35
|
+
},
|
|
36
|
+
...downloadProgress && {
|
|
37
|
+
onDownloadProgress: (e) => downloadProgress(toProgress(e.loaded, e.total))
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const rawHeaders = res.headers;
|
|
41
|
+
return {
|
|
42
|
+
status: res.status,
|
|
43
|
+
data: res.data,
|
|
44
|
+
headers: rawHeaders.toJSON?.() ?? { ...res.headers }
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (axios.isCancel(error)) {
|
|
48
|
+
throw createAbortError(error?.message);
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
export {
|
|
54
|
+
axiosAdapter
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=axios.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/axios.ts"],"sourcesContent":["import axios from 'axios'\nimport { defineAdapter } from '../core'\nimport { createAbortError } from '../errors'\nimport type { AxiosRequestConfig } from 'axios'\nimport type { RequestConfig, RequestProgress } from '../types'\n\n/** 归一化 axios 进度事件:total 缺失/为 0 时 progress 为 undefined */\nfunction toProgress(loaded: number, total?: number | null): RequestProgress {\n const t = total && total > 0 ? total : undefined\n return {\n loaded,\n total: t,\n progress: t !== undefined ? Math.min(loaded / t, 1) : undefined,\n }\n}\n\n/**\n * Web 端 adapter 插件:基于 axios。按需导入,主入口不含本模块。\n * 经 defineAdapter 导出:可直接使用,也可 .use() 叠加装饰器(withRetry / withDedupe 等)。\n * - validateStatus: () => true:任何状态码都 resolve,404/500 等判断统一交给 core\n * - axios 原生支持 AbortSignal,取消语义在此翻译为统一的 AbortError\n */\nexport const axiosAdapter = defineAdapter(async (config: RequestConfig) => {\n const { uploadProgress, downloadProgress } = config.on ?? {}\n try {\n const res = await axios.request({\n // 专有配置先展开(如 adapter: 'fetch'):契约派生字段在后覆盖,\n // url/method/headers/timeout/signal/validateStatus 始终以 RequestConfig 为准\n ...(config.adapterOptions?.axios as AxiosRequestConfig | undefined),\n url: config.url,\n method: config.method ?? 'GET',\n data: config.data,\n headers: config.headers,\n timeout: config.timeout,\n signal: config.signal,\n validateStatus: () => true,\n // axios 1.x 浏览器(XHR)与 Node(流式)适配器均产生进度事件\n ...(uploadProgress && {\n onUploadProgress: (e: { loaded: number; total?: number | null }) =>\n uploadProgress(toProgress(e.loaded, e.total)),\n }),\n ...(downloadProgress && {\n onDownloadProgress: (e: { loaded: number; total?: number | null }) =>\n downloadProgress(toProgress(e.loaded, e.total)),\n }),\n })\n // AxiosHeaders → 普通 Record,保持与其他 adapter 形态一致\n const rawHeaders = res.headers as unknown as {\n toJSON?: () => Record<string, string>\n }\n return {\n status: res.status,\n data: res.data,\n headers: rawHeaders.toJSON?.() ?? { ...res.headers },\n }\n } catch (error) {\n // 取消(signal abort)→ 统一 AbortError;其余(网络错误/超时)原样抛给 core 包装\n if (axios.isCancel(error)) {\n throw createAbortError((error as Error)?.message)\n }\n throw error\n }\n})\n"],"mappings":";;;;;;;;AAAA,OAAO,WAAW;AAOlB,SAAS,WAAW,QAAgB,OAAwC;AAC1E,QAAM,IAAI,SAAS,QAAQ,IAAI,QAAQ;AACvC,SAAO;AAAA,IACL;AAAA,IACA,OAAO;AAAA,IACP,UAAU,MAAM,SAAY,KAAK,IAAI,SAAS,GAAG,CAAC,IAAI;AAAA,EACxD;AACF;AAQO,IAAM,eAAe,cAAc,OAAO,WAA0B;AACzE,QAAM,EAAE,gBAAgB,iBAAiB,IAAI,OAAO,MAAM,CAAC;AAC3D,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,QAAQ;AAAA;AAAA;AAAA,MAG9B,GAAI,OAAO,gBAAgB;AAAA,MAC3B,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO,UAAU;AAAA,MACzB,MAAM,OAAO;AAAA,MACb,SAAS,OAAO;AAAA,MAChB,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,MACf,gBAAgB,MAAM;AAAA;AAAA,MAEtB,GAAI,kBAAkB;AAAA,QACpB,kBAAkB,CAAC,MACjB,eAAe,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC;AAAA,MAChD;AAAA,MACA,GAAI,oBAAoB;AAAA,QACtB,oBAAoB,CAAC,MACnB,iBAAiB,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC;AAAA,MAClD;AAAA,IACF,CAAC;AAED,UAAM,aAAa,IAAI;AAGvB,WAAO;AAAA,MACL,QAAQ,IAAI;AAAA,MACZ,MAAM,IAAI;AAAA,MACV,SAAS,WAAW,SAAS,KAAK,EAAE,GAAG,IAAI,QAAQ;AAAA,IACrD;AAAA,EACF,SAAS,OAAO;AAEd,QAAI,MAAM,SAAS,KAAK,GAAG;AACzB,YAAM,iBAAkB,OAAiB,OAAO;AAAA,IAClD;AACA,UAAM;AAAA,EACR;AACF,CAAC;","names":[]}
|