@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 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
- ### 1. 安装
9
+ [完整 API 文档](https://github.com/huangqi-an/request/blob/main/docs/api.md)
32
10
 
33
- **npm(推荐)**:
11
+ ## 安装
34
12
 
35
13
  ```bash
36
14
  npm install @huangqi_an/request
37
- # 仅使用 axiosAdapter 时需要自备 axios(optional peer dependency):
15
+ # 使用 axiosAdapter 时需要自行安装 axios(optional peer dependency):
38
16
  npm install axios
39
17
  ```
40
18
 
41
- **源码直引**:把 `src/` 拷进项目(或作为 monorepo 共享包引入),零构建;
42
- 本文示例中的 `@huangqi_an/request/...` 换成相对路径(如 `./src`)即可。
19
+ 也可将 `src/` 复制到项目中以源码形式使用(导入路径替换为相对路径)。
43
20
 
44
- ### 2. 创建实例并发请求(Web / Node / SSR)
21
+ ## 快速开始
22
+
23
+ Web / Node.js / SSR:
45
24
 
46
25
  ```ts
47
- import { createRequest } from './src'
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
- adapter: fetchAdapter, // 零依赖;Node 18+ / 现代浏览器
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 // 响应体(JSON 自动解析,视 adapter 而定)
36
+ res.data // 响应体
60
37
  res.status // 200
61
- res.headers // Record<string, string>
62
- res.durationMs // 耗时(core 注入)
38
+ res.durationMs // 耗时(毫秒)
63
39
  ```
64
40
 
65
- ### 3. uni-app
41
+ uni-app:
66
42
 
67
43
  ```ts
68
- import { createRequest } from './src'
69
- import { uniappAdapter } from './src/adapters/uniapp'
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
- ### 4. 统一错误处理
78
-
79
- 所有失败最终抛出同一个 `RequestError`,按 `kind` 四分类:
50
+ 错误处理——所有失败抛出 `RequestError`,按 `kind` 分类:
80
51
 
81
52
  ```ts
82
- import { RequestError } from './src'
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 / 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
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
- interface RawResponse<T = unknown> {
185
- status: number // HTTP 状态码
186
- data: T // 响应体
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
- `durationMs` / `config` 由 core 统一注入,三端形态一致;`res.config.headers` 可取到
194
- requestId 拦截器注入的请求 ID(报障定位),响应链也可按 url 做条件逻辑。
195
-
196
- ### RequestError
197
-
198
- | 字段 | 说明 |
79
+ | 拦截器 | 作用 |
199
80
  | --- | --- |
200
- | `kind` | `'abort' \| 'http' \| 'business' \| 'network'`,见下表 |
201
- | `status?` | HTTP 状态码(http / business 时存在) |
202
- | `data?` | 响应体或原始 body |
203
- | `headers?` | 响应头(http 时) |
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
- // 请求链:按注册顺序执行,返回新 config(注入 token / 请求 ID 等)
89
+ // 请求链:入参 config,返回改写后的 config(可异步)
220
90
  http.interceptors.request.use((config) => ({
221
91
  ...config,
222
- headers: { ...config.headers, Authorization: `Bearer ${getToken()}` },
92
+ headers: { ...config.headers, 'X-Locale': 'zh-CN' },
223
93
  }))
224
94
 
225
- // 响应链(成功):转换 / 剥壳
226
- http.interceptors.response.use((res) => ({ ...res, data: res.data.data }))
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
- report(err) // 无返回值 = 放行
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
- ### 组合:use 链
111
+ 通过 `adapter.use()` 叠加,注册顺序即请求经过顺序。
318
112
 
319
- 适配器经 `defineAdapter` 导出,本身可直接用,也可 `.use()` 叠加装饰器。
320
- **注册顺序 = 请求流经顺序**(与拦截器链一致):`.use(withDedupe()).use(withRetry())`
321
- 从上往下读就是请求经过的顺序——先去重(合并并发),再去重窗口内重试。
113
+ | 装饰器 | 作用 |
114
+ | --- | --- |
115
+ | `withRetry` | 失败自动重试;默认仅幂等方法(GET/HEAD),写方法默认不重试 |
116
+ | `withDedupe` | 进行中的相同请求共享结果;默认仅幂等方法,写方法默认不去重 |
322
117
 
323
118
  ```ts
324
- import { axiosAdapter } from './src/adapters/axios'
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
- `.use` 返回新适配器、不修改原对象,分支链互不影响。
333
-
334
- ### withRetry
335
-
336
- 失败自动重试。与错误链恢复不同:重试成功的响应会正常走完整管道(响应链剥壳等)。
122
+ 自定义装饰器实现 `AdapterEnhancer` 类型——一个"接收适配器、返回适配器"的函数:
337
123
 
338
124
  ```ts
339
- import { withRetry } from './src/enhancers/retry'
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
- ```ts
358
- import { withDedupe } from './src/enhancers/dedupe'
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(withDedupe()) // GET 去重
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 生态(拦截器插件、老浏览器兼容、`adapterOptions.axios.adapter` 切换传输)时 |
386
- | `uniappAdapter` | 全局 `uni` | uni-app 项目(H5 / 小程序 / App) |
387
- | 自定义 | — | wx 原生小程序、Taro、Electron net……按契约接入(见下) |
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. 输入 `RequestConfig`——其中的 `url` 已由 core 拼好 params,是完整地址
421
- 2. 返回 `RawResponse`(`status` / `data` / `headers`),**任何状态码都 resolve,判断交给 core**
422
- 3. 取消时抛 `name='AbortError'` 的错误,其余错误随意抛(core 统一包装为 `kind: 'network'`,`raw` 保留原始错误)
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 { createRequest, defineAdapter } from './src'
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.use(withRetry()) })
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
- 冲突规则:`url / method / headers / timeout / signal / validateStatus` 等契约派生字段始终以
484
- `RequestConfig` 为准,透传项覆盖不了它们——统一语义不被破坏。
165
+ 平台特有方法(如小程序的 `uploadFile`)通过 `.extend()` 附加到适配器上,附加方法可经过完整
166
+ 管道;错误链、事件、透传的用法见 [API 文档](https://github.com/huangqi-an/request/blob/main/docs/api.md)。
485
167
 
486
- 实例级默认透传用请求拦截器即可,不需要额外 API:
168
+ 平台差异:三个适配器的超时与取消行为一致;上传进度 fetch 不支持;PATCH 在微信小程序受
169
+ `wx.request` 方法白名单限制。详见[能力矩阵](https://github.com/huangqi-an/request/blob/main/docs/api.md#能力矩阵)。
487
170
 
488
- ```ts
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
- ```bash
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)
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  defineAdapter
3
- } from "../chunk-DQI4SM2Q.js";
3
+ } from "../chunk-4NADBDEH.js";
4
4
  import {
5
5
  createAbortError
6
6
  } from "../chunk-Z4N22SLG.js";