@huangqi_an/request 0.1.0 → 0.3.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/README.md +82 -435
- package/dist/adapters/axios.js +1 -1
- package/dist/adapters/fetch.js +4 -104
- package/dist/adapters/fetch.js.map +1 -1
- package/dist/adapters/uniapp.js +1 -1
- package/dist/{chunk-DQI4SM2Q.js → chunk-4NADBDEH.js} +17 -7
- package/dist/chunk-4NADBDEH.js.map +1 -0
- package/dist/chunk-6VCNLK2D.js +110 -0
- package/dist/chunk-6VCNLK2D.js.map +1 -0
- package/dist/chunk-HCW5TODA.js +38 -0
- package/dist/chunk-HCW5TODA.js.map +1 -0
- package/dist/chunk-RFWI3BY6.js +48 -0
- package/dist/chunk-RFWI3BY6.js.map +1 -0
- package/dist/chunk-Y45YDTWD.js +1 -0
- package/dist/chunk-Y45YDTWD.js.map +1 -0
- package/dist/core.d.ts +5 -0
- package/dist/core.js +1 -1
- package/dist/enhancers/dedupe.js +3 -33
- package/dist/enhancers/dedupe.js.map +1 -1
- package/dist/enhancers/retry.js +3 -42
- package/dist/enhancers/retry.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +31 -2
- package/dist/interceptors/index.d.ts +1 -1
- package/dist/interceptors/index.js +1 -0
- package/dist/{types-CoJSp-ar.d.ts → types-DLO2d3bV.d.ts} +17 -8
- package/package.json +2 -1
- package/dist/chunk-DQI4SM2Q.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,526 +1,173 @@
|
|
|
1
|
-
# request
|
|
2
|
-
|
|
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-平台选择) / [自定义适配器](#自定义适配器)
|
|
24
|
-
- [进阶](#进阶)
|
|
25
|
-
- [统一取消](#统一取消) / [事件](#事件) / [adapterOptions 透传](#adapteroptions-透传)
|
|
26
|
-
- [设计决策](#设计决策)
|
|
27
|
-
- [开发](#开发)
|
|
1
|
+
# @huangqi_an/request
|
|
28
2
|
|
|
29
|
-
|
|
3
|
+
跨平台 HTTP 请求库。同一套调用代码运行于浏览器、Node.js 与 uni-app,平台差异由适配器处理。
|
|
4
|
+
|
|
5
|
+
- 核心零依赖;适配器、拦截器、装饰器均为插件,按需导入
|
|
6
|
+
- 统一行为:状态码判定、错误分类(`abort` / `http` / `business` / `network`)、取消、超时、进度事件、拦截器
|
|
7
|
+
- TypeScript 编写,含完整类型声明
|
|
30
8
|
|
|
31
|
-
|
|
9
|
+
[完整 API 文档](https://github.com/huangqi-an/request/blob/main/docs/api.md)
|
|
32
10
|
|
|
33
|
-
|
|
11
|
+
## 安装
|
|
34
12
|
|
|
35
13
|
```bash
|
|
36
14
|
npm install @huangqi_an/request
|
|
37
|
-
#
|
|
15
|
+
# 使用 axiosAdapter 时需要自行安装 axios(optional peer dependency):
|
|
38
16
|
npm install axios
|
|
39
17
|
```
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
本文示例中的 `@huangqi_an/request/...` 换成相对路径(如 `./src`)即可。
|
|
19
|
+
也可将 `src/` 复制到项目中以源码形式使用(导入路径替换为相对路径)。
|
|
43
20
|
|
|
44
|
-
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
Web / Node.js / SSR:
|
|
45
24
|
|
|
46
25
|
```ts
|
|
47
|
-
import { createRequest } from '
|
|
48
|
-
import { fetchAdapter } from './src/adapters/fetch'
|
|
26
|
+
import { createRequest, fetchAdapter, withRetry, withDedupe } from '@huangqi_an/request'
|
|
49
27
|
|
|
50
28
|
export const http = createRequest({
|
|
51
|
-
|
|
29
|
+
// 装饰器可选;不使用时传入 fetchAdapter 即可
|
|
30
|
+
adapter: fetchAdapter.use(withDedupe()).use(withRetry()),
|
|
52
31
|
baseURL: 'https://api.example.com',
|
|
53
32
|
timeout: 15000,
|
|
54
|
-
headers: { 'X-Client': 'web' },
|
|
55
33
|
})
|
|
56
34
|
|
|
57
|
-
// 发请求
|
|
58
35
|
const res = await http.get('/users', { params: { page: 1 } })
|
|
59
|
-
res.data //
|
|
36
|
+
res.data // 响应体
|
|
60
37
|
res.status // 200
|
|
61
|
-
res.
|
|
62
|
-
res.durationMs // 耗时(core 注入)
|
|
38
|
+
res.durationMs // 耗时(毫秒)
|
|
63
39
|
```
|
|
64
40
|
|
|
65
|
-
|
|
41
|
+
uni-app:
|
|
66
42
|
|
|
67
43
|
```ts
|
|
68
|
-
import { createRequest } from '
|
|
69
|
-
import { uniappAdapter } from '
|
|
44
|
+
import { createRequest } from '@huangqi_an/request'
|
|
45
|
+
import { uniappAdapter } from '@huangqi_an/request/adapters/uniapp'
|
|
70
46
|
|
|
71
|
-
export const http = createRequest({
|
|
72
|
-
adapter: uniappAdapter, // 基于 uni.request,H5 / 小程序 / App 通用
|
|
73
|
-
baseURL: 'https://api.example.com',
|
|
74
|
-
})
|
|
47
|
+
export const http = createRequest({ adapter: uniappAdapter, baseURL: 'https://api.example.com' })
|
|
75
48
|
```
|
|
76
49
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
所有失败最终抛出同一个 `RequestError`,按 `kind` 四分类:
|
|
50
|
+
错误处理——所有失败抛出 `RequestError`,按 `kind` 分类:
|
|
80
51
|
|
|
81
52
|
```ts
|
|
82
|
-
import { RequestError } from '
|
|
53
|
+
import { RequestError } from '@huangqi_an/request'
|
|
83
54
|
|
|
84
55
|
try {
|
|
85
56
|
await http.post('/orders', { sku: 1 })
|
|
86
57
|
} catch (err) {
|
|
87
58
|
if (err instanceof RequestError) {
|
|
88
59
|
switch (err.kind) {
|
|
89
|
-
case 'http': // 非 2xx:err.status / err.data
|
|
90
|
-
|
|
91
|
-
case '
|
|
92
|
-
|
|
93
|
-
case 'abort': // 请求被取消
|
|
94
|
-
break
|
|
95
|
-
case 'network': // 传输层失败(含超时):err.raw 保留原始错误可排查
|
|
96
|
-
break
|
|
60
|
+
case 'http': break // 非 2xx:err.status / err.data
|
|
61
|
+
case 'business': break // 业务码失败:err.data 为原始响应体
|
|
62
|
+
case 'abort': break // 请求被取消
|
|
63
|
+
case 'network': break // 传输失败(含超时):err.raw 为原始错误
|
|
97
64
|
}
|
|
98
65
|
}
|
|
99
66
|
}
|
|
100
67
|
```
|
|
101
68
|
|
|
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)` | 全量配置发请求 |
|
|
156
|
-
| `get<T>(url, config?)` | 快捷方法,`config` 中省略 url/method |
|
|
157
|
-
| `post<T>(url, data?, config?)` | 同上,省略 url/method/data |
|
|
158
|
-
| `put<T>(url, data?, config?)` | 同上 |
|
|
159
|
-
| `delete<T>(url, config?)` | 同上 |
|
|
160
|
-
| `http.interceptors.request.use(fn)` | 注册请求链拦截器 |
|
|
161
|
-
| `http.interceptors.response.use(fn)` | 注册响应链拦截器(成功转换) |
|
|
162
|
-
| `http.interceptors.error.use(fn)` | 注册错误链拦截器 |
|
|
163
|
-
|
|
164
|
-
全部请求方法返回 `Promise<RawResponse<T>>`。
|
|
165
|
-
|
|
166
|
-
### RequestConfig
|
|
69
|
+
## 拦截器
|
|
167
70
|
|
|
168
|
-
|
|
169
|
-
| --- | --- | --- |
|
|
170
|
-
| `url` | `string` | 必填;相对 url 拼 baseURL,绝对 url 直接使用 |
|
|
171
|
-
| `method` | `RequestMethod` | 默认 `'GET'` |
|
|
172
|
-
| `baseURL` | `string` | 请求级覆盖实例级 |
|
|
173
|
-
| `params` | `Record<string, unknown>` | query 参数,core 统一序列化(数组重复 key、跳过 null/undefined、encodeURIComponent) |
|
|
174
|
-
| `data` | `unknown` | 请求体;fetch 端对象/数组自动 JSON 序列化并补 `Content-Type` |
|
|
175
|
-
| `headers` | `Record<string, string>` | 与实例级浅合并,请求级优先 |
|
|
176
|
-
| `timeout` | `number` | 毫秒 |
|
|
177
|
-
| `signal` | `AbortSignal` | 统一取消(见[统一取消](#统一取消)) |
|
|
178
|
-
| `on` | `RequestEvents` | 事件订阅(见[事件](#事件)) |
|
|
179
|
-
| `adapterOptions` | `AdapterOptions` | 平台专有参数透传(见[adapterOptions 透传](#adapteroptions-透传)) |
|
|
180
|
-
|
|
181
|
-
### RawResponse
|
|
71
|
+
三条链:`request`(改写请求)、`response`(转换响应)、`error`(处理失败,可恢复)。
|
|
182
72
|
|
|
183
73
|
```ts
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
headers: Record<string, string>
|
|
188
|
-
durationMs?: number // core 注入:adapter 调用耗时
|
|
189
|
-
config?: RequestConfig // core 注入:最终请求配置(url 已拼好 params)
|
|
190
|
-
}
|
|
74
|
+
http.interceptors.request.use(createAuthInterceptor({ getToken }))
|
|
75
|
+
http.interceptors.request.use(createRequestIdInterceptor())
|
|
76
|
+
http.interceptors.response.use(createUnwrapInterceptor())
|
|
191
77
|
```
|
|
192
78
|
|
|
193
|
-
|
|
194
|
-
requestId 拦截器注入的请求 ID(报障定位),响应链也可按 url 做条件逻辑。
|
|
195
|
-
|
|
196
|
-
### RequestError
|
|
197
|
-
|
|
198
|
-
| 字段 | 说明 |
|
|
79
|
+
| 拦截器 | 作用 |
|
|
199
80
|
| --- | --- |
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `config?` | 最终请求配置(可取请求 ID 等注入头) |
|
|
205
|
-
| `raw?` | 原始错误:axios 的 `error.code`(如 `ECONNABORTED` 超时)、uni 的 `errMsg` |
|
|
206
|
-
|
|
207
|
-
| kind | 含义 | 来源 |
|
|
208
|
-
| --- | --- | --- |
|
|
209
|
-
| `http` | 收到响应但状态码非 2xx | core |
|
|
210
|
-
| `business` | HTTP 2xx 但业务码失败 | unwrap 拦截器 |
|
|
211
|
-
| `abort` | 请求被取消(AbortSignal / 平台取消) | 各适配器 |
|
|
212
|
-
| `network` | 传输层失败,含超时 | core 包装适配器抛错 |
|
|
213
|
-
|
|
214
|
-
## 拦截器
|
|
81
|
+
| `createAuthInterceptor` | 注入凭证头 |
|
|
82
|
+
| `createRequestIdInterceptor` | 为每个请求注入唯一 ID |
|
|
83
|
+
| `createUnwrapInterceptor` | 剥离 `{code, data, message}` 业务壳,业务码失败抛 `kind: 'business'` |
|
|
84
|
+
| `createLoggerInterceptors` | 三链日志 |
|
|
215
85
|
|
|
216
|
-
|
|
86
|
+
自定义拦截器就是一个符合链签名的函数,无需任何注册 API 之外的封装:
|
|
217
87
|
|
|
218
88
|
```ts
|
|
219
|
-
//
|
|
89
|
+
// 请求链:入参 config,返回改写后的 config(可异步)
|
|
220
90
|
http.interceptors.request.use((config) => ({
|
|
221
91
|
...config,
|
|
222
|
-
headers: { ...config.headers,
|
|
92
|
+
headers: { ...config.headers, 'X-Locale': 'zh-CN' },
|
|
223
93
|
}))
|
|
224
94
|
|
|
225
|
-
//
|
|
226
|
-
http.interceptors.response.use((res) =>
|
|
95
|
+
// 响应链:入参 RawResponse,返回 RawResponse;抛出的错误进入错误链
|
|
96
|
+
http.interceptors.response.use((res) => {
|
|
97
|
+
if (res.status === 204) return { ...res, data: null }
|
|
98
|
+
return res
|
|
99
|
+
})
|
|
227
100
|
|
|
228
|
-
//
|
|
101
|
+
// 错误链:入参 RequestError。返回 undefined 表示放行,返回 RawResponse 表示恢复
|
|
229
102
|
http.interceptors.error.use((err) => {
|
|
230
|
-
|
|
103
|
+
if (err.kind === 'network') notify('网络异常')
|
|
231
104
|
})
|
|
232
105
|
```
|
|
233
106
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
1. 三条链相互独立,均**按注册顺序**执行
|
|
237
|
-
2. 响应链任一环节抛错,进入错误链
|
|
238
|
-
3. 错误链入口先归一化:所有失败(请求拦截器抛错 / 网络失败 / 非 2xx / 响应链抛错)都是 `RequestError`
|
|
239
|
-
4. 错误链行为:**返回 `RawResponse` = 恢复**(结束错误链,直接作为最终结果);**返回/抛出 `RequestError` = 替换**当前错误继续走链;**无返回值 = 放行**(纯观察)
|
|
240
|
-
5. 要"所有错误都上报",上报拦截器必须**最先注册**——恢复发生后就不再通知后续拦截器
|
|
241
|
-
|
|
242
|
-
### createAuthInterceptor
|
|
243
|
-
|
|
244
|
-
请求链:从 `getToken()` 注入凭证头,支持异步(如先静默刷新)。
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
import { createAuthInterceptor } from './src/interceptors'
|
|
248
|
-
|
|
249
|
-
http.interceptors.request.use(
|
|
250
|
-
createAuthInterceptor({
|
|
251
|
-
getToken: () => getToken(),
|
|
252
|
-
shouldAttach: (config) => !config.url.includes('/login'),
|
|
253
|
-
}),
|
|
254
|
-
)
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
| 选项 | 默认 | 说明 |
|
|
258
|
-
| --- | --- | --- |
|
|
259
|
-
| `getToken` | 必传 | 返回 token(可异步);返回空值则跳过注入 |
|
|
260
|
-
| `headerName` | `'Authorization'` | 注入的头名 |
|
|
261
|
-
| `scheme` | `'Bearer '` | token 前缀;不需要传空字符串 |
|
|
262
|
-
| `shouldAttach(config)` | — | 返回 false 的请求不注入 |
|
|
263
|
-
|
|
264
|
-
### createRequestIdInterceptor
|
|
265
|
-
|
|
266
|
-
请求链:每个请求生成唯一 ID 注入请求头,用于前后端日志关联与报障定位。
|
|
267
|
-
|
|
268
|
-
```ts
|
|
269
|
-
import { createRequestIdInterceptor } from './src/interceptors'
|
|
270
|
-
|
|
271
|
-
http.interceptors.request.use(createRequestIdInterceptor())
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
- core 会把最终 config 盖章到 `RawResponse.config` 与 `RequestError.config`,响应日志、错误上报都能取到同一个 ID
|
|
275
|
-
- 重试共享同一 config:同一次逻辑请求(含重试)带同一个 ID,服务端可识别重发
|
|
276
|
-
|
|
277
|
-
| 选项 | 默认 | 说明 |
|
|
278
|
-
| --- | --- | --- |
|
|
279
|
-
| `headerName` | `'X-Request-Id'` | 请求头名 |
|
|
280
|
-
| `generateId()` | `crypto.randomUUID()` | 运行时缺失时退化为时间戳+随机数;可接入 tracing/Sentry 等链路库 |
|
|
281
|
-
| `keepExisting` | `false` | 已带该头时不覆盖(上游链路透传)。注意:实例级默认头也算"已携带",开启会导致所有请求共用一个 ID,慎用 |
|
|
282
|
-
|
|
283
|
-
### createUnwrapInterceptor
|
|
284
|
-
|
|
285
|
-
响应链:剥离 `{code, data, message}` 业务壳;业务码失败抛 `kind: 'business'` 的 `RequestError`(`data` 为原始 body,进入错误链)。
|
|
286
|
-
|
|
287
|
-
```ts
|
|
288
|
-
import { createUnwrapInterceptor } from './src/interceptors'
|
|
289
|
-
|
|
290
|
-
http.interceptors.response.use(createUnwrapInterceptor())
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
| 选项 | 默认 | 说明 |
|
|
294
|
-
| --- | --- | --- |
|
|
295
|
-
| `successCodes` | `[0]` | 业务成功码,允许字符串码 |
|
|
296
|
-
| `codeKey` / `dataKey` / `messageKey` | `'code' / 'data' / 'message'` | 字段名 |
|
|
297
|
-
| — | — | body 非对象或缺业务码字段时原样放行(防御非 JSON 响应) |
|
|
298
|
-
|
|
299
|
-
### createLoggerInterceptors
|
|
300
|
-
|
|
301
|
-
三链日志:返回 `{ request, response, error }` 三件套,分别注册。
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
import { createLoggerInterceptors } from './src/interceptors'
|
|
305
|
-
|
|
306
|
-
const logger = createLoggerInterceptors() // 默认 console
|
|
307
|
-
http.interceptors.request.use(logger.request)
|
|
308
|
-
http.interceptors.response.use(logger.response)
|
|
309
|
-
http.interceptors.error.use(logger.error)
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
- request:method / url / params;response:status / 耗时 / url;error:kind / message / url(warn 级别)
|
|
313
|
-
- 选项 `log(level, ...args)` 可替换为埋点 SDK;注入的函数**不应抛错**
|
|
107
|
+
各选项与完整语义见 [API 文档 · 拦截器](https://github.com/huangqi-an/request/blob/main/docs/api.md#拦截器)。
|
|
314
108
|
|
|
315
|
-
##
|
|
109
|
+
## 装饰器
|
|
316
110
|
|
|
317
|
-
|
|
111
|
+
通过 `adapter.use()` 叠加,注册顺序即请求经过顺序。
|
|
318
112
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
113
|
+
| 装饰器 | 作用 |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `withRetry` | 失败自动重试;默认仅幂等方法(GET/HEAD),写方法默认不重试 |
|
|
116
|
+
| `withDedupe` | 进行中的相同请求共享结果;默认仅幂等方法,写方法默认不去重 |
|
|
322
117
|
|
|
323
118
|
```ts
|
|
324
|
-
|
|
325
|
-
import { withRetry } from './src/enhancers/retry'
|
|
326
|
-
import { withDedupe } from './src/enhancers/dedupe'
|
|
327
|
-
|
|
328
|
-
const adapter = axiosAdapter.use(withDedupe()).use(withRetry({ retries: 2 }))
|
|
329
|
-
const http = createRequest({ adapter })
|
|
119
|
+
const adapter = fetchAdapter.use(withDedupe()).use(withRetry({ retries: 2 }))
|
|
330
120
|
```
|
|
331
121
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
### withRetry
|
|
335
|
-
|
|
336
|
-
失败自动重试。与错误链恢复不同:重试成功的响应会正常走完整管道(响应链剥壳等)。
|
|
122
|
+
自定义装饰器实现 `AdapterEnhancer` 类型——一个"接收适配器、返回适配器"的函数:
|
|
337
123
|
|
|
338
124
|
```ts
|
|
339
|
-
import {
|
|
340
|
-
|
|
341
|
-
const adapter = fetchAdapter.use(withRetry({ retries: 2, delayMs: (n) => n * 300 }))
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
| 选项 | 默认 | 说明 |
|
|
345
|
-
| --- | --- | --- |
|
|
346
|
-
| `retries` | `2` | 最大重试次数(不含首次;总尝试 = 1 + retries) |
|
|
347
|
-
| `methods` | `['GET', 'HEAD']` | 默认重试规则覆盖的方法 |
|
|
348
|
-
| `shouldRetry({error?, response?}, attempt, config)` | — | 提供后**完全接管**默认规则(`methods` 不再生效) |
|
|
349
|
-
| `delayMs(attempt)` | 不等待 | 第 attempt 次重试前的等待毫秒 |
|
|
350
|
-
|
|
351
|
-
默认规则:抛错且方法幂等 → 重试;非 2xx 响应、取消 → 不重试(取消无论配置如何都不重试)。
|
|
352
|
-
|
|
353
|
-
### withDedupe
|
|
354
|
-
|
|
355
|
-
进行中的相同请求共享同一个 Promise(settle 后自动清理)。默认只对幂等方法生效。
|
|
125
|
+
import type { AdapterEnhancer } from '@huangqi_an/request'
|
|
356
126
|
|
|
357
|
-
|
|
358
|
-
|
|
127
|
+
const withTiming: AdapterEnhancer = (adapter) => async (config) => {
|
|
128
|
+
const start = Date.now()
|
|
129
|
+
const res = await adapter(config)
|
|
130
|
+
console.log(`${config.method} ${config.url} ${Date.now() - start}ms`)
|
|
131
|
+
return res
|
|
132
|
+
}
|
|
359
133
|
|
|
360
|
-
const adapter = fetchAdapter.use(
|
|
361
|
-
const antiDoubleSubmit = fetchAdapter.use(withDedupe({ methods: ['POST'] })) // 防双击
|
|
134
|
+
const adapter = fetchAdapter.use(withTiming)
|
|
362
135
|
```
|
|
363
136
|
|
|
364
|
-
|
|
365
|
-
| --- | --- | --- |
|
|
366
|
-
| `methods` | `['GET', 'HEAD']` | 参与去重的方法;写方法默认放行 |
|
|
367
|
-
| `key(config)` | `` `${method} ${url} ${JSON.stringify(data)}` `` | params 已拼进 url;data 无法序列化时该请求不参与去重(宁可漏,不可错合并) |
|
|
368
|
-
|
|
369
|
-
### 幂等性默认
|
|
370
|
-
|
|
371
|
-
两个装饰器都只对幂等方法(GET/HEAD)默认生效——
|
|
372
|
-
|
|
373
|
-
- dedupe 的 share 语义不会"多发",但会"少发":对故意并发的写请求(如计数 +1)静默合并等于丢操作
|
|
374
|
-
- retry 对写方法自动重发,可能重复执行已到达服务端的请求(如重复扣款)
|
|
375
|
-
|
|
376
|
-
所以写方法默认放行 / 不重试;需要时显式 opt-in(dedupe 传 `methods: ['POST']` 做防双击、retry 提供自定义 `shouldRetry`)。资金类接口请以后端幂等键(Idempotency-Key)兜底,前端只是第一道闸。
|
|
137
|
+
完整选项与语义见 [API 文档 · 适配器装饰器](https://github.com/huangqi-an/request/blob/main/docs/api.md#适配器装饰器)。
|
|
377
138
|
|
|
378
139
|
## 适配器
|
|
379
140
|
|
|
380
|
-
### 选型
|
|
381
|
-
|
|
382
141
|
| 适配器 | 依赖 | 适用 |
|
|
383
142
|
| --- | --- | --- |
|
|
384
|
-
| `fetchAdapter` | 无 | Web / Node 18+ / SSR
|
|
385
|
-
| `axiosAdapter` | axios | 需要 axios
|
|
386
|
-
| `uniappAdapter` | 全局 `uni` | uni-app
|
|
387
|
-
| 自定义 | — | wx 原生小程序、Taro、Electron
|
|
388
|
-
|
|
389
|
-
### 能力矩阵
|
|
390
|
-
|
|
391
|
-
平台差异在适配器内抹平,做不到的静默不触发;超时与取消三端行为一致。
|
|
392
|
-
|
|
393
|
-
| 适配器 | 上传进度 | 下载进度 | 说明 |
|
|
394
|
-
| --- | --- | --- | --- |
|
|
395
|
-
| `axiosAdapter` | ✓ | ✓ | axios 1.x 浏览器 XHR 与 Node 流式适配器均支持 |
|
|
396
|
-
| `uniappAdapter` | ✓ | ✓ | `task.onProgressUpdate` 归一化(旧基础库缺失该 API 时跳过) |
|
|
397
|
-
| `fetchAdapter` | ✗ | ✓ | fetch 无上传进度(平台限制);下载走 `res.body` 流式读取,`Content-Length` 缺失时 `total/progress` 为 undefined |
|
|
398
|
-
|
|
399
|
-
### uni-app 平台选择
|
|
400
|
-
|
|
401
|
-
注意:**uni-app ≠ 原生微信小程序**。本库的 `uniappAdapter` 依赖 uni-app 全局 `uni.request`;
|
|
402
|
-
原生小程序用的是 `wx.request`,那是另一套 API,需要单独的适配器(按契约实现即可,见下)。
|
|
403
|
-
uni-app 项目用条件编译分别创建实例(导入路径按你放置 `src/` 的位置调整):
|
|
404
|
-
|
|
405
|
-
```ts
|
|
406
|
-
// #ifdef H5
|
|
407
|
-
import { fetchAdapter } from '@/request/adapters/fetch'
|
|
408
|
-
export const http = createRequest({ adapter: fetchAdapter })
|
|
409
|
-
// #endif
|
|
410
|
-
// #ifdef MP-WEIXIN
|
|
411
|
-
import { uniappAdapter } from '@/request/adapters/uniapp'
|
|
412
|
-
export const http = createRequest({ adapter: uniappAdapter })
|
|
413
|
-
// #endif
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
### 自定义适配器
|
|
143
|
+
| `fetchAdapter` | 无 | Web / Node.js 18+ / SSR |
|
|
144
|
+
| `axiosAdapter` | axios | 需要 axios 生态时(从 `@huangqi_an/request/adapters/axios` 导入) |
|
|
145
|
+
| `uniappAdapter` | 全局 `uni` | uni-app(H5 / 小程序 / App),从 `@huangqi_an/request/adapters/uniapp` 导入 |
|
|
146
|
+
| 自定义 | — | wx 原生小程序、Taro、Electron 等,按契约实现 |
|
|
417
147
|
|
|
418
|
-
|
|
148
|
+
自定义适配器契约共三条:
|
|
419
149
|
|
|
420
|
-
1.
|
|
421
|
-
2. 返回 `RawResponse`(`status` / `data` / `headers
|
|
422
|
-
3.
|
|
423
|
-
|
|
424
|
-
`defineAdapter` 把实现函数包装为可链式增强的适配器(参数获得类型推断):
|
|
150
|
+
1. 入参为 `RequestConfig`,其中 `url` 已拼接完成,是完整地址。
|
|
151
|
+
2. 返回 `RawResponse`(`status` / `data` / `headers`),任意状态码都 resolve,状态码判定由 core 负责。
|
|
152
|
+
3. 取消时抛出 `name='AbortError'` 的错误;其余错误直接抛出,core 统一包装为 `kind: 'network'`。
|
|
425
153
|
|
|
426
154
|
```ts
|
|
427
|
-
import {
|
|
155
|
+
import { defineAdapter } from '@huangqi_an/request'
|
|
428
156
|
|
|
429
157
|
const myAdapter = defineAdapter(async (config) => {
|
|
430
158
|
const res = await myTransport.request(config.url, config)
|
|
431
159
|
return { status: res.status, data: res.body, headers: res.headers }
|
|
432
160
|
})
|
|
433
161
|
|
|
434
|
-
const http = createRequest({ adapter: myAdapter
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
可运行的完整示例见 `tests/custom-adapter.test.ts`(一个与内置 `adapters/fetch.ts` 同构的 fetch 实现)。
|
|
438
|
-
|
|
439
|
-
## 进阶
|
|
440
|
-
|
|
441
|
-
### 统一取消
|
|
442
|
-
|
|
443
|
-
所有适配器都走标准 `AbortSignal`:
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
const controller = new AbortController()
|
|
447
|
-
http.get('/heavy-report', { signal: controller.signal }).catch(() => {})
|
|
448
|
-
controller.abort() // axios:原生取消;uni.request:桥接到 task.abort();fetch:原生取消
|
|
449
|
-
```
|
|
450
|
-
|
|
451
|
-
### 事件
|
|
452
|
-
|
|
453
|
-
事件通过 `config.on` 订阅,进度事件统一为 `{ loaded, total?, progress? }`(`progress` 为 0-1,`total` 未知时为 undefined):
|
|
454
|
-
|
|
455
|
-
```ts
|
|
456
|
-
await http.post('/upload', formData, {
|
|
457
|
-
on: {
|
|
458
|
-
uploadProgress: ({ progress }) => bar.update(progress),
|
|
459
|
-
},
|
|
460
|
-
})
|
|
461
|
-
await http.get('/big-file', {
|
|
462
|
-
on: {
|
|
463
|
-
downloadProgress: ({ loaded, total }) => console.log(loaded, total),
|
|
464
|
-
},
|
|
465
|
-
})
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
**扩展策略**:`on` 是事件的唯一入口——以后新增事件(如 `headersReceived`、`chunkReceived`、`retry`)
|
|
469
|
-
只扩展 `RequestEvents` 接口并在对应适配器中 emit,`RequestConfig` 结构保持封闭。
|
|
470
|
-
|
|
471
|
-
### adapterOptions 透传
|
|
472
|
-
|
|
473
|
-
平台特有参数不进 `RequestConfig` 根,统一走 `adapterOptions`(按适配器名取用、仅对应适配器消费):
|
|
474
|
-
|
|
475
|
-
```ts
|
|
476
|
-
// 让 axios 走它自己的 fetch 适配器(axios 1.7+)
|
|
477
|
-
await http.get('/x', { adapterOptions: { axios: { adapter: 'fetch' } } })
|
|
478
|
-
|
|
479
|
-
// fetch 携带 cookie / 控制缓存
|
|
480
|
-
await http.get('/me', { adapterOptions: { fetch: { credentials: 'include', cache: 'no-store' } } })
|
|
162
|
+
const http = createRequest({ adapter: myAdapter })
|
|
481
163
|
```
|
|
482
164
|
|
|
483
|
-
|
|
484
|
-
|
|
165
|
+
平台特有方法(如小程序的 `uploadFile`)通过 `.extend()` 附加到适配器上,附加方法可经过完整
|
|
166
|
+
管道;错误链、事件、透传的用法见 [API 文档](https://github.com/huangqi-an/request/blob/main/docs/api.md)。
|
|
485
167
|
|
|
486
|
-
|
|
168
|
+
平台差异:三个适配器的超时与取消行为一致;上传进度 fetch 不支持;PATCH 在微信小程序受
|
|
169
|
+
`wx.request` 方法白名单限制。详见[能力矩阵](https://github.com/huangqi-an/request/blob/main/docs/api.md#能力矩阵)。
|
|
487
170
|
|
|
488
|
-
|
|
489
|
-
http.interceptors.request.use((config) => ({
|
|
490
|
-
...config,
|
|
491
|
-
adapterOptions: { ...config.adapterOptions, axios: { adapter: 'fetch' } },
|
|
492
|
-
}))
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
## 设计决策
|
|
496
|
-
|
|
497
|
-
| 决策 | 原因 |
|
|
498
|
-
| --- | --- |
|
|
499
|
-
| 主入口零依赖,插件按需导入 | 依赖各自携带,未被 import 的模块不进 bundle;小程序包体积与平台耦合由结构保证,而非约定 |
|
|
500
|
-
| 适配器只返回 `RawResponse`,不做任何判断 | 404/500 在两端表现不同(axios 抛错、uni 走 success),统一 resolve 交给 core 判 2xx,是三端语义归一的支点 |
|
|
501
|
-
| params 在 core 拼成完整 URL | 三端行为完全一致(数组重复 key、跳过 null/undefined、encode 规则),适配器保持"哑" |
|
|
502
|
-
| 取消统一为 `name='AbortError'` 的 Error | uni-app 非 H5 运行时(小程序/App 端)没有 `DOMException`;`e.name === 'AbortError'` 的判定方式各端兼容 |
|
|
503
|
-
| core dispatch 前先查 `signal.aborted` | 防止"已取消还发请求"的竞态 |
|
|
504
|
-
| 错误独立成链,undefined 即放行 | 观察(上报/toast)与恢复/替换同一入口;观察类钩子不必被迫 rethrow |
|
|
505
|
-
| 事件走 `RequestEvents`,`RequestConfig` 结构封闭 | 新增事件只扩接口并在适配器 emit,契约不再膨胀 |
|
|
506
|
-
| `adapterOptions` 逃生舱,契约字段优先 | 平台特有参数有统一去处,且统一语义(状态码/取消/超时)不可被透传破坏 |
|
|
507
|
-
| retry / dedupe 默认仅幂等方法 | 写请求宁可放行(不去重、不重试),绝不静默吞操作或重发非幂等操作 |
|
|
508
|
-
| 错误带 `raw` 字段 | 保留 axios 的 `error.code`(如 `ECONNABORTED` 超时)和 uni 的 `errMsg`,排查不丢信息 |
|
|
509
|
-
|
|
510
|
-
## 开发
|
|
171
|
+
## License
|
|
511
172
|
|
|
512
|
-
|
|
513
|
-
npm install
|
|
514
|
-
npm run test # vitest,86 个用例
|
|
515
|
-
npm run typecheck # tsc --noEmit
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
| 测试文件 | 覆盖 |
|
|
519
|
-
| --- | --- |
|
|
520
|
-
| `tests/core.test.ts` | mock 适配器:配置合并、拦截器顺序、params 拼接、错误四分类、错误链恢复/替换/放行 |
|
|
521
|
-
| `tests/adapter.fetch.test.ts` | 真实 fetch 打本地 server:形状、404、body 序列化、进度(含 chunked)、取消、超时、透传 |
|
|
522
|
-
| `tests/adapter.axios.test.ts` | 真实 axios 打本地 server:形状、404、进度、`adapter: 'fetch'` 切换、透传冲突规则 |
|
|
523
|
-
| `tests/adapter.uniapp.test.ts` | fake `globalThis.uni`:success/fail 映射、500 透传、取消、进度归一化、透传 |
|
|
524
|
-
| `tests/interceptors.test.ts` | auth / unwrap / logger / requestId 全选项覆盖 |
|
|
525
|
-
| `tests/enhancers.test.ts` | retry/dedupe 全场景(含幂等默认)+ `.use` 链式语义 |
|
|
526
|
-
| `tests/custom-adapter.test.ts` | 第三方按契约接入的可行性证明 |
|
|
173
|
+
[MIT](./LICENSE)
|