@djvlc/openapi-client-core 1.2.0 → 1.2.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 +996 -146
- package/dist/index.d.mts +2635 -191
- package/dist/index.d.ts +2635 -191
- package/dist/index.js +2764 -338
- package/dist/index.mjs +2681 -333
- package/package.json +2 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -2,41 +2,523 @@
|
|
|
2
2
|
|
|
3
3
|
OpenAPI Client 公共运行时库,为 `@djvlc/openapi-*-client` 系列包提供核心功能。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **结构化错误类**:`DjvApiError`、`NetworkError`、`TimeoutError`、`AbortError`
|
|
8
|
-
- **增强版 fetch**:超时、重试(支持 Retry-After)、取消支持
|
|
9
|
-
- **Middleware 系统**:请求/响应拦截
|
|
10
|
-
- **拦截器系统**:更灵活的请求/响应/错误处理
|
|
11
|
-
- **请求去重**:避免相同 GET 请求重复发送
|
|
12
|
-
- **指标收集**:请求级别的性能指标和统计
|
|
13
|
-
- **日志系统**:可配置的请求日志
|
|
14
|
-
- **客户端工厂**:简化客户端创建
|
|
15
|
-
- **自动版本注入**:构建时自动从 package.json 注入版本号
|
|
5
|
+
## 特性
|
|
16
6
|
|
|
17
|
-
|
|
7
|
+
- 🔌 **模块化架构**:按功能拆分,每个模块职责单一
|
|
8
|
+
- 🔗 **拦截器系统**:请求/响应/错误拦截器,支持顺序控制
|
|
9
|
+
- 🔐 **多种认证**:Bearer/API Key/Basic/自定义认证
|
|
10
|
+
- 🔄 **智能重试**:支持 Retry-After、指数退避、自定义策略
|
|
11
|
+
- 🎯 **请求去重**:避免相同请求并发执行
|
|
12
|
+
- 📊 **指标收集**:请求级别的性能指标和统计
|
|
13
|
+
- 📝 **日志系统**:可配置的请求日志
|
|
14
|
+
- 🛡️ **类型安全**:完整的 TypeScript 类型定义
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
## 安装
|
|
20
17
|
|
|
21
18
|
```bash
|
|
22
19
|
pnpm add @djvlc/openapi-client-core
|
|
23
20
|
```
|
|
24
21
|
|
|
22
|
+
## 模块结构
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── types/ # 类型定义
|
|
27
|
+
├── errors/ # 错误处理
|
|
28
|
+
├── auth/ # 认证模块
|
|
29
|
+
├── interceptors/ # 拦截器
|
|
30
|
+
│ ├── request/ # 请求拦截器
|
|
31
|
+
│ ├── response/ # 响应拦截器
|
|
32
|
+
│ └── error/ # 错误拦截器
|
|
33
|
+
├── plugins/ # 插件
|
|
34
|
+
├── clients/ # HTTP 客户端
|
|
35
|
+
├── utils/ # 工具函数
|
|
36
|
+
└── index.ts # 主入口
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 快速开始
|
|
40
|
+
|
|
41
|
+
### 使用 Fetch 客户端
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createFetchClient } from '@djvlc/openapi-client-core';
|
|
45
|
+
|
|
46
|
+
const client = createFetchClient({
|
|
47
|
+
baseUrl: 'https://api.example.com',
|
|
48
|
+
timeout: 30000,
|
|
49
|
+
auth: {
|
|
50
|
+
type: 'bearer',
|
|
51
|
+
getToken: () => localStorage.getItem('token'),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// 发起请求
|
|
56
|
+
const data = await client.get<User>('/users/me');
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 使用 Axios 客户端(推荐用于 openapi-generator)
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { createAxiosInstance } from '@djvlc/openapi-client-core';
|
|
63
|
+
import { PagesApi, Configuration } from './generated';
|
|
64
|
+
|
|
65
|
+
const axiosInstance = createAxiosInstance({
|
|
66
|
+
baseUrl: '/api/admin',
|
|
67
|
+
auth: {
|
|
68
|
+
type: 'bearer',
|
|
69
|
+
getToken: () => localStorage.getItem('token'),
|
|
70
|
+
},
|
|
71
|
+
retry: {
|
|
72
|
+
maxRetries: 2,
|
|
73
|
+
initialDelayMs: 1000,
|
|
74
|
+
maxDelayMs: 10000,
|
|
75
|
+
backoff: 'exponential',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const config = new Configuration();
|
|
80
|
+
const pagesApi = new PagesApi(config, undefined, axiosInstance);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## HTTP 客户端详解
|
|
84
|
+
|
|
85
|
+
本库提供两种 HTTP 客户端实现,共享相同的配置接口,功能完全对等:
|
|
86
|
+
|
|
87
|
+
### 客户端对比
|
|
88
|
+
|
|
89
|
+
| 特性 | FetchClient | Axios Client |
|
|
90
|
+
|------|-------------|--------------|
|
|
91
|
+
| **依赖** | 无(原生 fetch) | 需要 axios |
|
|
92
|
+
| **使用场景** | 独立使用、轻量场景 | OpenAPI 生成代码集成 |
|
|
93
|
+
| **Bundle 大小** | 更小 | 需包含 axios |
|
|
94
|
+
| **请求拦截器** | ✅ 支持 | ✅ 支持 |
|
|
95
|
+
| **响应拦截器** | ✅ 支持 | ✅ 支持 |
|
|
96
|
+
| **错误拦截器** | ✅ 支持 | ✅ 支持 |
|
|
97
|
+
| **认证** | ✅ 全类型 | ✅ 全类型 |
|
|
98
|
+
| **自动重试** | ✅ 支持 | ✅ 支持 |
|
|
99
|
+
| **超时控制** | ✅ AbortController | ✅ 原生 timeout |
|
|
100
|
+
| **请求取消** | ✅ AbortSignal | ✅ CancelToken |
|
|
101
|
+
| **调试日志** | ✅ 支持 | ✅ 支持 |
|
|
102
|
+
| **Retry-After** | ✅ 尊重 | ✅ 尊重 |
|
|
103
|
+
|
|
104
|
+
### 如何选择?
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
108
|
+
│ 你的使用场景是什么? │
|
|
109
|
+
└─────────────────────────────────────────────────────────────┘
|
|
110
|
+
│
|
|
111
|
+
┌───────────────┴───────────────┐
|
|
112
|
+
▼ ▼
|
|
113
|
+
使用 OpenAPI 生成的代码? 独立发起 HTTP 请求?
|
|
114
|
+
│ │
|
|
115
|
+
▼ ▼
|
|
116
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
117
|
+
│ Axios Client │ │ FetchClient │
|
|
118
|
+
│ (推荐) │ │ (推荐) │
|
|
119
|
+
└─────────────────┘ └─────────────────┘
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### createEnhancedFetch(推荐用于 OpenAPI 生成代码)
|
|
123
|
+
|
|
124
|
+
`createEnhancedFetch` 创建一个增强的 fetch 函数,专为与 `typescript-fetch` 生成的 API 类集成设计:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { createEnhancedFetch, createConsoleLogger } from '@djvlc/openapi-client-core';
|
|
128
|
+
import { PagesApi, Configuration } from '@djvlc/openapi-admin-client';
|
|
129
|
+
|
|
130
|
+
// 创建增强的 fetch 函数
|
|
131
|
+
const enhancedFetch = createEnhancedFetch({
|
|
132
|
+
baseUrl: '/api/admin',
|
|
133
|
+
timeout: 30000,
|
|
134
|
+
|
|
135
|
+
// 认证
|
|
136
|
+
auth: {
|
|
137
|
+
type: 'bearer',
|
|
138
|
+
getToken: () => localStorage.getItem('token') ?? '',
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// 重试
|
|
142
|
+
retry: {
|
|
143
|
+
maxRetries: 3,
|
|
144
|
+
backoffStrategy: 'exponential',
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// 日志
|
|
148
|
+
logger: createConsoleLogger({ prefix: '[API]' }),
|
|
149
|
+
debug: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 与生成的 API 类集成
|
|
153
|
+
const config = new Configuration({ basePath: '/api/admin' });
|
|
154
|
+
const pagesApi = new PagesApi(config, '/api/admin', enhancedFetch);
|
|
155
|
+
|
|
156
|
+
// 使用
|
|
157
|
+
const pages = await pagesApi.listPages();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**增强功能包括:**
|
|
161
|
+
- ✅ 自动认证(Bearer/API Key/Basic/自定义)
|
|
162
|
+
- ✅ 智能重试(指数退避 + Retry-After)
|
|
163
|
+
- ✅ 超时控制(AbortController)
|
|
164
|
+
- ✅ 请求追踪(X-Request-ID, X-Trace-ID)
|
|
165
|
+
- ✅ 错误转换(ApiError, NetworkError, TimeoutError)
|
|
166
|
+
- ✅ 调试日志
|
|
167
|
+
|
|
168
|
+
### FetchClient 完整配置
|
|
169
|
+
|
|
170
|
+
`FetchClient` 基于原生 fetch API,适合独立使用(不依赖 OpenAPI 生成代码):
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import {
|
|
174
|
+
createFetchClient,
|
|
175
|
+
createConsoleLogger,
|
|
176
|
+
type FetchClientConfig,
|
|
177
|
+
type RequestInterceptor,
|
|
178
|
+
type ResponseInterceptor,
|
|
179
|
+
type ErrorInterceptor,
|
|
180
|
+
} from '@djvlc/openapi-client-core';
|
|
181
|
+
|
|
182
|
+
const config: FetchClientConfig = {
|
|
183
|
+
// ============ 基础配置 ============
|
|
184
|
+
|
|
185
|
+
/** 必填:API 基础 URL */
|
|
186
|
+
baseUrl: 'https://api.example.com',
|
|
187
|
+
|
|
188
|
+
/** 请求超时时间(毫秒),默认 30000 */
|
|
189
|
+
timeout: 30000,
|
|
190
|
+
|
|
191
|
+
/** 默认请求头 */
|
|
192
|
+
headers: {
|
|
193
|
+
'X-Custom-Header': 'value',
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// ============ 认证配置 ============
|
|
197
|
+
|
|
198
|
+
/** 认证配置 */
|
|
199
|
+
auth: {
|
|
200
|
+
type: 'bearer',
|
|
201
|
+
getToken: async () => await tokenService.getAccessToken(),
|
|
202
|
+
// 其他选项见 "认证" 章节
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// ============ 重试配置 ============
|
|
206
|
+
|
|
207
|
+
/** 是否启用自动重试,默认 true */
|
|
208
|
+
enableRetry: true,
|
|
209
|
+
|
|
210
|
+
/** 重试策略 */
|
|
211
|
+
retry: {
|
|
212
|
+
maxRetries: 3,
|
|
213
|
+
initialDelayMs: 1000,
|
|
214
|
+
maxDelayMs: 30000,
|
|
215
|
+
backoffStrategy: 'exponential',
|
|
216
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
217
|
+
retryOnNetworkError: true,
|
|
218
|
+
retryOnTimeout: true,
|
|
219
|
+
respectRetryAfter: true,
|
|
220
|
+
jitterFactor: 0.1,
|
|
221
|
+
onRetry: (info) => {
|
|
222
|
+
console.log(`重试 ${info.attempt}/${info.maxRetries}`);
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// ============ 拦截器 ============
|
|
227
|
+
|
|
228
|
+
/** 请求拦截器列表 */
|
|
229
|
+
requestInterceptors: [
|
|
230
|
+
{
|
|
231
|
+
name: 'custom-header',
|
|
232
|
+
order: 0,
|
|
233
|
+
intercept(context) {
|
|
234
|
+
context.options.headers['X-Request-Time'] = Date.now().toString();
|
|
235
|
+
return context.options;
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
|
|
240
|
+
/** 响应拦截器列表 */
|
|
241
|
+
responseInterceptors: [
|
|
242
|
+
{
|
|
243
|
+
name: 'response-logger',
|
|
244
|
+
order: 0,
|
|
245
|
+
intercept(response, context) {
|
|
246
|
+
console.log(`${context.options.method} ${context.options.path} -> ${response.status}`);
|
|
247
|
+
return response;
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
|
|
252
|
+
/** 错误拦截器列表 */
|
|
253
|
+
errorInterceptors: [
|
|
254
|
+
{
|
|
255
|
+
name: 'error-logger',
|
|
256
|
+
order: 0,
|
|
257
|
+
intercept(error, context) {
|
|
258
|
+
console.error(`请求失败: ${error.message}`);
|
|
259
|
+
return undefined; // 不处理,继续传播
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
|
|
264
|
+
// ============ 调试配置 ============
|
|
265
|
+
|
|
266
|
+
/** 日志器 */
|
|
267
|
+
logger: createConsoleLogger({ prefix: '[API]', level: 'debug' }),
|
|
268
|
+
|
|
269
|
+
/** 是否启用调试日志,默认 false */
|
|
270
|
+
debug: true,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const client = createFetchClient(config);
|
|
274
|
+
|
|
275
|
+
// 使用客户端
|
|
276
|
+
const user = await client.get<User>('/users/me');
|
|
277
|
+
const created = await client.post<User>('/users', { name: 'Alice' });
|
|
278
|
+
const updated = await client.put<User>('/users/1', { name: 'Bob' });
|
|
279
|
+
const patched = await client.patch<User>('/users/1', { name: 'Charlie' });
|
|
280
|
+
await client.delete('/users/1');
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Axios Client 完整配置
|
|
284
|
+
|
|
285
|
+
`createAxiosInstance` 返回一个配置好的 Axios 实例,适合与 OpenAPI 生成代码集成:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import {
|
|
289
|
+
createAxiosInstance,
|
|
290
|
+
createConsoleLogger,
|
|
291
|
+
type AxiosClientConfig,
|
|
292
|
+
} from '@djvlc/openapi-client-core';
|
|
293
|
+
import { PagesApi, ComponentsApi, Configuration } from '@djvlc/openapi-admin-client';
|
|
294
|
+
|
|
295
|
+
const config: AxiosClientConfig = {
|
|
296
|
+
// ============ 基础配置 ============
|
|
297
|
+
|
|
298
|
+
/** 必填:API 基础 URL */
|
|
299
|
+
baseUrl: '/api/admin',
|
|
300
|
+
|
|
301
|
+
/** 请求超时时间(毫秒),默认 30000 */
|
|
302
|
+
timeout: 30000,
|
|
303
|
+
|
|
304
|
+
/** 默认请求头 */
|
|
305
|
+
headers: {
|
|
306
|
+
'X-Client-Version': '1.0.0',
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
// ============ 认证配置 ============
|
|
310
|
+
|
|
311
|
+
auth: {
|
|
312
|
+
type: 'bearer',
|
|
313
|
+
getToken: () => localStorage.getItem('accessToken') ?? '',
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
// ============ 重试配置 ============
|
|
317
|
+
|
|
318
|
+
enableRetry: true,
|
|
319
|
+
|
|
320
|
+
retry: {
|
|
321
|
+
maxRetries: 3,
|
|
322
|
+
initialDelayMs: 1000,
|
|
323
|
+
maxDelayMs: 30000,
|
|
324
|
+
backoffStrategy: 'exponential',
|
|
325
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
326
|
+
onRetry: (info) => {
|
|
327
|
+
console.log(`[Retry] ${info.method} ${info.url} - attempt ${info.attempt}`);
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
// ============ 拦截器(与 FetchClient 相同) ============
|
|
332
|
+
|
|
333
|
+
requestInterceptors: [...],
|
|
334
|
+
responseInterceptors: [...],
|
|
335
|
+
errorInterceptors: [...],
|
|
336
|
+
|
|
337
|
+
// ============ 调试配置 ============
|
|
338
|
+
|
|
339
|
+
logger: createConsoleLogger({ prefix: '[Admin API]' }),
|
|
340
|
+
debug: process.env.NODE_ENV === 'development',
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// 创建 Axios 实例
|
|
344
|
+
const axiosInstance = createAxiosInstance(config);
|
|
345
|
+
|
|
346
|
+
// 与 OpenAPI 生成代码集成
|
|
347
|
+
const apiConfig = new Configuration();
|
|
348
|
+
const pagesApi = new PagesApi(apiConfig, undefined, axiosInstance);
|
|
349
|
+
const componentsApi = new ComponentsApi(apiConfig, undefined, axiosInstance);
|
|
350
|
+
|
|
351
|
+
// 使用生成的 API 客户端
|
|
352
|
+
const pages = await pagesApi.listPages({ limit: 10 });
|
|
353
|
+
const page = await pagesApi.getPage({ id: 'page-123' });
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### 配置选项速查表
|
|
357
|
+
|
|
358
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
359
|
+
|--------|------|--------|------|
|
|
360
|
+
| `baseUrl` | `string` | - | **必填**,API 基础 URL |
|
|
361
|
+
| `timeout` | `number` | `30000` | 请求超时(毫秒) |
|
|
362
|
+
| `headers` | `Record<string, string>` | `{}` | 默认请求头 |
|
|
363
|
+
| `auth` | `AuthConfig` | - | 认证配置 |
|
|
364
|
+
| `retry` | `RetryConfig` | - | 重试配置 |
|
|
365
|
+
| `enableRetry` | `boolean` | `true` | 是否启用重试 |
|
|
366
|
+
| `requestInterceptors` | `RequestInterceptor[]` | `[]` | 请求拦截器 |
|
|
367
|
+
| `responseInterceptors` | `ResponseInterceptor[]` | `[]` | 响应拦截器 |
|
|
368
|
+
| `errorInterceptors` | `ErrorInterceptor[]` | `[]` | 错误拦截器 |
|
|
369
|
+
| `logger` | `Logger` | - | 日志器实例 |
|
|
370
|
+
| `debug` | `boolean` | `false` | 是否启用调试日志 |
|
|
371
|
+
|
|
372
|
+
### 完整示例:生产环境配置
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import {
|
|
376
|
+
createAxiosInstance,
|
|
377
|
+
createConsoleLogger,
|
|
378
|
+
createMetricsCollector,
|
|
379
|
+
createMetricsInterceptors,
|
|
380
|
+
createLoggingInterceptor,
|
|
381
|
+
createReportingInterceptor,
|
|
382
|
+
ApiError,
|
|
383
|
+
} from '@djvlc/openapi-client-core';
|
|
384
|
+
|
|
385
|
+
// ============ 创建日志器 ============
|
|
386
|
+
const logger = createConsoleLogger({
|
|
387
|
+
prefix: '[DJV-API]',
|
|
388
|
+
level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug',
|
|
389
|
+
timestamp: true,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// ============ 创建指标收集器 ============
|
|
393
|
+
const metricsCollector = createMetricsCollector({
|
|
394
|
+
maxMetrics: 1000,
|
|
395
|
+
ttlMs: 300000, // 5 分钟
|
|
396
|
+
onMetrics: (m) => {
|
|
397
|
+
// 发送到监控系统
|
|
398
|
+
if (m.durationMs > 3000) {
|
|
399
|
+
logger.warn(`慢请求: ${m.method} ${m.path} - ${m.durationMs}ms`);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const metricsInterceptors = createMetricsInterceptors(metricsCollector);
|
|
405
|
+
|
|
406
|
+
// ============ 创建错误上报拦截器 ============
|
|
407
|
+
const errorReporter = createReportingInterceptor({
|
|
408
|
+
reporter: {
|
|
409
|
+
report: (error, context) => {
|
|
410
|
+
// 只上报服务端错误
|
|
411
|
+
if (ApiError.is(error) && error.isServerError()) {
|
|
412
|
+
Sentry.captureException(error, {
|
|
413
|
+
extra: {
|
|
414
|
+
requestId: context.requestId,
|
|
415
|
+
traceId: context.traceId,
|
|
416
|
+
url: context.url,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
sampleRate: 1.0, // 生产环境 100% 采样
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// ============ 创建客户端 ============
|
|
426
|
+
const axiosInstance = createAxiosInstance({
|
|
427
|
+
baseUrl: import.meta.env.VITE_API_BASE_URL,
|
|
428
|
+
timeout: 30000,
|
|
429
|
+
|
|
430
|
+
auth: {
|
|
431
|
+
type: 'bearer',
|
|
432
|
+
getToken: () => authStore.getAccessToken(),
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
retry: {
|
|
436
|
+
maxRetries: 3,
|
|
437
|
+
initialDelayMs: 1000,
|
|
438
|
+
maxDelayMs: 30000,
|
|
439
|
+
backoffStrategy: 'exponential',
|
|
440
|
+
jitterFactor: 0.2,
|
|
441
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
442
|
+
respectRetryAfter: true,
|
|
443
|
+
onRetry: (info) => {
|
|
444
|
+
logger.info(`重试请求: ${info.method} ${info.url} (${info.attempt}/${info.maxRetries})`);
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
requestInterceptors: [
|
|
449
|
+
metricsInterceptors.request,
|
|
450
|
+
{
|
|
451
|
+
name: 'app-version',
|
|
452
|
+
order: 100,
|
|
453
|
+
intercept(ctx) {
|
|
454
|
+
ctx.options.headers['X-App-Version'] = APP_VERSION;
|
|
455
|
+
ctx.options.headers['X-Platform'] = 'web';
|
|
456
|
+
return ctx.options;
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
|
|
461
|
+
responseInterceptors: [
|
|
462
|
+
metricsInterceptors.response,
|
|
463
|
+
],
|
|
464
|
+
|
|
465
|
+
errorInterceptors: [
|
|
466
|
+
metricsInterceptors.error,
|
|
467
|
+
errorReporter,
|
|
468
|
+
{
|
|
469
|
+
name: 'auth-redirect',
|
|
470
|
+
order: 1000,
|
|
471
|
+
intercept(error, context) {
|
|
472
|
+
if (ApiError.is(error) && error.isUnauthorized()) {
|
|
473
|
+
// Token 过期,跳转登录
|
|
474
|
+
authStore.logout();
|
|
475
|
+
router.push('/login');
|
|
476
|
+
}
|
|
477
|
+
return undefined;
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
|
|
482
|
+
logger,
|
|
483
|
+
debug: import.meta.env.DEV,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ============ 定期上报指标 ============
|
|
487
|
+
setInterval(() => {
|
|
488
|
+
const summary = metricsCollector.summary();
|
|
489
|
+
analytics.track('api_metrics', {
|
|
490
|
+
totalRequests: summary.totalRequests,
|
|
491
|
+
successRate: summary.successRate,
|
|
492
|
+
p50: summary.p50Ms,
|
|
493
|
+
p95: summary.p95Ms,
|
|
494
|
+
p99: summary.p99Ms,
|
|
495
|
+
});
|
|
496
|
+
}, 60000);
|
|
497
|
+
|
|
498
|
+
export { axiosInstance, metricsCollector };
|
|
499
|
+
```
|
|
500
|
+
|
|
25
501
|
## 错误处理
|
|
26
502
|
|
|
27
503
|
```typescript
|
|
28
|
-
import {
|
|
504
|
+
import {
|
|
505
|
+
ApiError,
|
|
506
|
+
NetworkError,
|
|
507
|
+
TimeoutError,
|
|
508
|
+
AbortError,
|
|
509
|
+
isRetryableError,
|
|
510
|
+
} from '@djvlc/openapi-client-core';
|
|
29
511
|
|
|
30
512
|
try {
|
|
31
|
-
await client.
|
|
513
|
+
await client.get('/users/me');
|
|
32
514
|
} catch (e) {
|
|
33
|
-
if (
|
|
515
|
+
if (ApiError.is(e)) {
|
|
34
516
|
// 业务错误(服务器返回的错误响应)
|
|
35
517
|
console.log('业务错误:', e.code, e.message);
|
|
36
|
-
console.log('HTTP 状态码:', e.
|
|
518
|
+
console.log('HTTP 状态码:', e.statusCode);
|
|
37
519
|
console.log('追踪 ID:', e.traceId);
|
|
38
520
|
|
|
39
|
-
//
|
|
521
|
+
// 状态码判断
|
|
40
522
|
if (e.isAuthError()) {
|
|
41
523
|
// 认证错误 (401/403)
|
|
42
524
|
redirectToLogin();
|
|
@@ -55,10 +537,15 @@ try {
|
|
|
55
537
|
} else if (NetworkError.is(e)) {
|
|
56
538
|
console.log('网络错误:', e.message);
|
|
57
539
|
}
|
|
540
|
+
|
|
541
|
+
// 判断是否可重试
|
|
542
|
+
if (isRetryableError(e)) {
|
|
543
|
+
console.log('此错误可以重试');
|
|
544
|
+
}
|
|
58
545
|
}
|
|
59
546
|
```
|
|
60
547
|
|
|
61
|
-
###
|
|
548
|
+
### ApiError 方法
|
|
62
549
|
|
|
63
550
|
| 方法 | 说明 |
|
|
64
551
|
|---|---|
|
|
@@ -74,37 +561,325 @@ try {
|
|
|
74
561
|
| `getRetryAfter()` | 获取 Retry-After 头的值(秒) |
|
|
75
562
|
| `getRetryDelayMs(default)` | 获取重试延迟(毫秒) |
|
|
76
563
|
|
|
77
|
-
##
|
|
564
|
+
## 认证
|
|
565
|
+
|
|
566
|
+
### Bearer Token
|
|
78
567
|
|
|
79
568
|
```typescript
|
|
80
|
-
import {
|
|
569
|
+
import { createFetchClient } from '@djvlc/openapi-client-core';
|
|
81
570
|
|
|
82
|
-
const client =
|
|
571
|
+
const client = createFetchClient({
|
|
83
572
|
baseUrl: 'https://api.example.com',
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
respectRetryAfter: true, // 尊重 Retry-After 响应头(默认 true)
|
|
90
|
-
onRetry: (error, attempt, delayMs) => {
|
|
91
|
-
console.log('第', attempt, '次重试,延迟', delayMs, 'ms');
|
|
573
|
+
auth: {
|
|
574
|
+
type: 'bearer',
|
|
575
|
+
getToken: async () => {
|
|
576
|
+
// 支持异步获取 Token
|
|
577
|
+
return await tokenService.getAccessToken();
|
|
92
578
|
},
|
|
579
|
+
headerName: 'Authorization', // 可选,默认 'Authorization'
|
|
580
|
+
prefix: 'Bearer', // 可选,默认 'Bearer'
|
|
93
581
|
},
|
|
94
582
|
});
|
|
95
583
|
```
|
|
96
584
|
|
|
97
|
-
|
|
585
|
+
### API Key
|
|
98
586
|
|
|
99
|
-
|
|
587
|
+
```typescript
|
|
588
|
+
const client = createFetchClient({
|
|
589
|
+
baseUrl: 'https://api.example.com',
|
|
590
|
+
auth: {
|
|
591
|
+
type: 'api-key',
|
|
592
|
+
apiKey: 'your-api-key',
|
|
593
|
+
headerName: 'X-API-Key', // 可选
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Basic Auth
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
const client = createFetchClient({
|
|
602
|
+
baseUrl: 'https://api.example.com',
|
|
603
|
+
auth: {
|
|
604
|
+
type: 'basic',
|
|
605
|
+
username: 'admin',
|
|
606
|
+
password: 'secret',
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### 自定义认证
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
const client = createFetchClient({
|
|
615
|
+
baseUrl: 'https://api.example.com',
|
|
616
|
+
auth: {
|
|
617
|
+
type: 'custom',
|
|
618
|
+
authenticate: async (headers) => {
|
|
619
|
+
headers['X-Custom-Auth'] = await getCustomToken();
|
|
620
|
+
headers['X-Timestamp'] = Date.now().toString();
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
## 拦截器
|
|
627
|
+
|
|
628
|
+
### 请求拦截器
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
import {
|
|
632
|
+
createFetchClient,
|
|
633
|
+
createAuthInterceptor,
|
|
634
|
+
createRequestIdInterceptor,
|
|
635
|
+
createTraceInterceptor,
|
|
636
|
+
createBearerAuthenticator,
|
|
637
|
+
} from '@djvlc/openapi-client-core';
|
|
638
|
+
|
|
639
|
+
const client = createFetchClient({
|
|
640
|
+
baseUrl: 'https://api.example.com',
|
|
641
|
+
requestInterceptors: [
|
|
642
|
+
// 请求 ID 拦截器
|
|
643
|
+
createRequestIdInterceptor({
|
|
644
|
+
headerName: 'X-Request-ID',
|
|
645
|
+
}),
|
|
646
|
+
|
|
647
|
+
// 追踪拦截器
|
|
648
|
+
createTraceInterceptor({
|
|
649
|
+
traceIdHeader: 'X-Trace-ID',
|
|
650
|
+
addTraceparent: true,
|
|
651
|
+
getTraceparent: () => otel.getCurrentSpan()?.spanContext(),
|
|
652
|
+
}),
|
|
653
|
+
],
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### 错误拦截器
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import {
|
|
661
|
+
createFetchClient,
|
|
662
|
+
createLoggingInterceptor,
|
|
663
|
+
createReportingInterceptor,
|
|
664
|
+
createConsoleLogger,
|
|
665
|
+
} from '@djvlc/openapi-client-core';
|
|
666
|
+
|
|
667
|
+
const client = createFetchClient({
|
|
668
|
+
baseUrl: 'https://api.example.com',
|
|
669
|
+
errorInterceptors: [
|
|
670
|
+
// 日志拦截器
|
|
671
|
+
createLoggingInterceptor({
|
|
672
|
+
logger: createConsoleLogger({ prefix: '[API]' }),
|
|
673
|
+
verbose: true,
|
|
674
|
+
}),
|
|
675
|
+
|
|
676
|
+
// 错误上报拦截器
|
|
677
|
+
createReportingInterceptor({
|
|
678
|
+
reporter: {
|
|
679
|
+
report: (error, context) => {
|
|
680
|
+
Sentry.captureException(error, { extra: context });
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
sampleRate: 0.1, // 10% 采样
|
|
684
|
+
}),
|
|
685
|
+
],
|
|
686
|
+
});
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### 自定义拦截器
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
import type { RequestInterceptor, ErrorInterceptor } from '@djvlc/openapi-client-core';
|
|
693
|
+
|
|
694
|
+
// 自定义请求拦截器
|
|
695
|
+
const customRequestInterceptor: RequestInterceptor = {
|
|
696
|
+
name: 'custom-request',
|
|
697
|
+
order: 0,
|
|
698
|
+
intercept(context) {
|
|
699
|
+
// 修改请求
|
|
700
|
+
context.options.headers['X-Custom'] = 'value';
|
|
701
|
+
return context.options;
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// 自定义错误拦截器
|
|
706
|
+
const customErrorInterceptor: ErrorInterceptor = {
|
|
707
|
+
name: 'custom-error',
|
|
708
|
+
order: 0,
|
|
709
|
+
intercept(error, context) {
|
|
710
|
+
console.log('请求失败:', context.url, error.message);
|
|
711
|
+
return undefined; // 不处理,让错误继续传播
|
|
712
|
+
},
|
|
713
|
+
};
|
|
100
714
|
|
|
101
|
-
|
|
715
|
+
const client = createFetchClient({
|
|
716
|
+
baseUrl: 'https://api.example.com',
|
|
717
|
+
requestInterceptors: [customRequestInterceptor],
|
|
718
|
+
errorInterceptors: [customErrorInterceptor],
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
## 重试配置
|
|
723
|
+
|
|
724
|
+
### 基础用法
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
const client = createFetchClient({
|
|
728
|
+
baseUrl: 'https://api.example.com',
|
|
729
|
+
enableRetry: true, // 启用重试,默认 true
|
|
730
|
+
retry: {
|
|
731
|
+
maxRetries: 3,
|
|
732
|
+
initialDelayMs: 1000,
|
|
733
|
+
maxDelayMs: 30000,
|
|
734
|
+
backoffStrategy: 'exponential',
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### 完整配置选项
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
import type { RetryConfig, RetryInfo, RetryContext } from '@djvlc/openapi-client-core';
|
|
743
|
+
|
|
744
|
+
const retryConfig: RetryConfig = {
|
|
745
|
+
// ============ 基础参数 ============
|
|
746
|
+
|
|
747
|
+
/** 最大重试次数,默认 3 */
|
|
748
|
+
maxRetries: 3,
|
|
749
|
+
|
|
750
|
+
/** 初始延迟(毫秒),默认 1000 */
|
|
751
|
+
initialDelayMs: 1000,
|
|
752
|
+
|
|
753
|
+
/** 最大延迟(毫秒),默认 30000 */
|
|
754
|
+
maxDelayMs: 30000,
|
|
755
|
+
|
|
756
|
+
// ============ 退避策略 ============
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* 退避策略
|
|
760
|
+
* - 'fixed': 固定延迟,每次重试使用 initialDelayMs
|
|
761
|
+
* - 'linear': 线性增长,delay = initialDelayMs * attempt
|
|
762
|
+
* - 'exponential': 指数增长,delay = initialDelayMs * (2 ^ attempt)
|
|
763
|
+
* 默认 'exponential'
|
|
764
|
+
*/
|
|
765
|
+
backoffStrategy: 'exponential',
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* 抖动系数(0-1)
|
|
769
|
+
* 在计算的延迟基础上添加随机抖动,防止惊群效应
|
|
770
|
+
* 实际延迟 = delay * (1 + random(-jitterFactor, +jitterFactor))
|
|
771
|
+
* 默认 0.1
|
|
772
|
+
*/
|
|
773
|
+
jitterFactor: 0.1,
|
|
774
|
+
|
|
775
|
+
// ============ 重试条件 ============
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* 可重试的 HTTP 状态码
|
|
779
|
+
* 默认 [429, 500, 502, 503, 504]
|
|
780
|
+
*/
|
|
781
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
782
|
+
|
|
783
|
+
/** 网络错误是否重试,默认 true */
|
|
784
|
+
retryOnNetworkError: true,
|
|
785
|
+
|
|
786
|
+
/** 超时是否重试,默认 true */
|
|
787
|
+
retryOnTimeout: true,
|
|
788
|
+
|
|
789
|
+
/** 是否尊重 Retry-After 响应头,默认 true */
|
|
790
|
+
respectRetryAfter: true,
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* 自定义重试判断函数
|
|
794
|
+
* 返回 true 表示应该重试
|
|
795
|
+
*/
|
|
796
|
+
shouldRetry: (error: Error, attempt: number, context: RetryContext): boolean => {
|
|
797
|
+
// 例:只重试 GET 请求
|
|
798
|
+
if (context.method !== 'GET') return false;
|
|
799
|
+
|
|
800
|
+
// 例:429 错误最多重试 5 次
|
|
801
|
+
if (context.statusCode === 429 && attempt >= 5) return false;
|
|
802
|
+
|
|
803
|
+
return true;
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
// ============ 回调 ============
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* 重试时的回调函数
|
|
810
|
+
* 可用于日志、监控等
|
|
811
|
+
*/
|
|
812
|
+
onRetry: (info: RetryInfo) => {
|
|
813
|
+
console.log(`重试请求: ${info.method} ${info.url}`);
|
|
814
|
+
console.log(` - 第 ${info.attempt}/${info.maxRetries} 次`);
|
|
815
|
+
console.log(` - 延迟 ${info.delayMs}ms`);
|
|
816
|
+
console.log(` - 原因: ${info.error.message}`);
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### 退避策略对比
|
|
822
|
+
|
|
823
|
+
假设 `initialDelayMs = 1000`,`maxDelayMs = 30000`:
|
|
824
|
+
|
|
825
|
+
| 重试次数 | fixed | linear | exponential |
|
|
826
|
+
|----------|-------|--------|-------------|
|
|
827
|
+
| 第 1 次 | 1000ms | 1000ms | 1000ms |
|
|
828
|
+
| 第 2 次 | 1000ms | 2000ms | 2000ms |
|
|
829
|
+
| 第 3 次 | 1000ms | 3000ms | 4000ms |
|
|
830
|
+
| 第 4 次 | 1000ms | 4000ms | 8000ms |
|
|
831
|
+
| 第 5 次 | 1000ms | 5000ms | 16000ms |
|
|
832
|
+
| 第 6 次 | 1000ms | 6000ms | 30000ms (capped) |
|
|
833
|
+
|
|
834
|
+
### Retry-After 响应头
|
|
835
|
+
|
|
836
|
+
当 `respectRetryAfter: true` 时,客户端会检查响应头:
|
|
837
|
+
|
|
838
|
+
```http
|
|
839
|
+
HTTP/1.1 429 Too Many Requests
|
|
840
|
+
Retry-After: 60
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
客户端会等待 60 秒后重试(但不超过 `maxDelayMs`)。
|
|
844
|
+
|
|
845
|
+
支持的格式:
|
|
846
|
+
- 秒数:`Retry-After: 120`
|
|
847
|
+
- HTTP 日期:`Retry-After: Wed, 21 Oct 2025 07:28:00 GMT`
|
|
848
|
+
|
|
849
|
+
### 禁用重试
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
// 方式 1:全局禁用
|
|
853
|
+
const client = createFetchClient({
|
|
854
|
+
baseUrl: 'https://api.example.com',
|
|
855
|
+
enableRetry: false,
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// 方式 2:不配置 retry
|
|
859
|
+
const client = createFetchClient({
|
|
860
|
+
baseUrl: 'https://api.example.com',
|
|
861
|
+
// 不传 retry 配置
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// 方式 3:单次请求禁用(通过自定义 shouldRetry)
|
|
865
|
+
const retryConfig: RetryConfig = {
|
|
866
|
+
maxRetries: 3,
|
|
867
|
+
shouldRetry: (error, attempt, context) => {
|
|
868
|
+
// POST 请求不重试
|
|
869
|
+
if (context.method === 'POST') return false;
|
|
870
|
+
return true;
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
## 请求去重
|
|
102
876
|
|
|
103
877
|
```typescript
|
|
104
878
|
import { createRequestDeduper } from '@djvlc/openapi-client-core';
|
|
105
879
|
|
|
106
880
|
const deduper = createRequestDeduper({
|
|
107
|
-
getOnly: true,
|
|
881
|
+
getOnly: true, // 只对 GET 请求去重
|
|
882
|
+
maxSize: 100, // 最大缓存数量
|
|
108
883
|
});
|
|
109
884
|
|
|
110
885
|
// 包装 fetch
|
|
@@ -116,6 +891,8 @@ const [r1, r2] = await Promise.all([
|
|
|
116
891
|
dedupedFetch('/api/user'),
|
|
117
892
|
]);
|
|
118
893
|
// 只发送了 1 个请求!
|
|
894
|
+
|
|
895
|
+
console.log('当前飞行中请求数:', deduper.inflightCount);
|
|
119
896
|
```
|
|
120
897
|
|
|
121
898
|
## 指标收集
|
|
@@ -123,23 +900,27 @@ const [r1, r2] = await Promise.all([
|
|
|
123
900
|
```typescript
|
|
124
901
|
import {
|
|
125
902
|
createMetricsCollector,
|
|
126
|
-
|
|
903
|
+
createMetricsInterceptors,
|
|
904
|
+
createFetchClient,
|
|
127
905
|
} from '@djvlc/openapi-client-core';
|
|
128
906
|
|
|
129
907
|
// 创建指标收集器
|
|
130
908
|
const metrics = createMetricsCollector({
|
|
131
|
-
maxMetrics: 1000,
|
|
132
|
-
ttlMs: 3600000,
|
|
909
|
+
maxMetrics: 1000,
|
|
910
|
+
ttlMs: 3600000, // 1 小时过期
|
|
133
911
|
onMetrics: (m) => {
|
|
134
|
-
|
|
135
|
-
console.log(m.method, m.path, '-', m.durationMs, 'ms');
|
|
912
|
+
console.log(`${m.method} ${m.path} - ${m.durationMs}ms`);
|
|
136
913
|
},
|
|
137
914
|
});
|
|
138
915
|
|
|
139
|
-
//
|
|
140
|
-
const
|
|
916
|
+
// 创建拦截器
|
|
917
|
+
const metricsInterceptors = createMetricsInterceptors(metrics);
|
|
918
|
+
|
|
919
|
+
const client = createFetchClient({
|
|
141
920
|
baseUrl: 'https://api.example.com',
|
|
142
|
-
|
|
921
|
+
requestInterceptors: [metricsInterceptors.request],
|
|
922
|
+
responseInterceptors: [metricsInterceptors.response],
|
|
923
|
+
errorInterceptors: [metricsInterceptors.error],
|
|
143
924
|
});
|
|
144
925
|
|
|
145
926
|
// 获取指标摘要
|
|
@@ -151,7 +932,7 @@ console.log('P95:', summary.p95Ms, 'ms');
|
|
|
151
932
|
console.log('P99:', summary.p99Ms, 'ms');
|
|
152
933
|
|
|
153
934
|
// 按路径查看
|
|
154
|
-
const pageMetrics = metrics.getByPath('/api/
|
|
935
|
+
const pageMetrics = metrics.getByPath('/api/pages');
|
|
155
936
|
|
|
156
937
|
// 清空并获取所有指标
|
|
157
938
|
const allMetrics = metrics.flush();
|
|
@@ -170,149 +951,218 @@ const allMetrics = metrics.flush();
|
|
|
170
951
|
| `durationMs` | number | 耗时(毫秒) |
|
|
171
952
|
| `status` | number | HTTP 状态码 |
|
|
172
953
|
| `success` | boolean | 是否成功 (2xx) |
|
|
173
|
-
| `retryCount` | number
|
|
954
|
+
| `retryCount` | number | 重试次数 |
|
|
174
955
|
| `traceId` | string? | 追踪 ID |
|
|
956
|
+
| `error` | string? | 错误信息 |
|
|
175
957
|
|
|
176
|
-
##
|
|
958
|
+
## 日志
|
|
177
959
|
|
|
178
960
|
```typescript
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// 在某个时机取消请求
|
|
182
|
-
setTimeout(() => controller.abort(), 5000);
|
|
961
|
+
import { createConsoleLogger, createBufferLogger } from '@djvlc/openapi-client-core';
|
|
183
962
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
if (AbortError.is(e)) {
|
|
191
|
-
console.log('请求已取消');
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
963
|
+
// 控制台日志器
|
|
964
|
+
const consoleLogger = createConsoleLogger({
|
|
965
|
+
prefix: '[DJV-API]',
|
|
966
|
+
level: 'debug', // 最低日志级别
|
|
967
|
+
timestamp: true, // 显示时间戳
|
|
968
|
+
});
|
|
195
969
|
|
|
196
|
-
|
|
970
|
+
// 缓冲日志器(用于测试)
|
|
971
|
+
const bufferLogger = createBufferLogger(1000);
|
|
197
972
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const client = createUserClient({
|
|
973
|
+
// 使用
|
|
974
|
+
const client = createFetchClient({
|
|
202
975
|
baseUrl: 'https://api.example.com',
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
prefix: '[MyApp-API]',
|
|
206
|
-
level: 'debug',
|
|
207
|
-
timestamp: true,
|
|
208
|
-
}),
|
|
976
|
+
logger: consoleLogger,
|
|
977
|
+
debug: true,
|
|
209
978
|
});
|
|
979
|
+
|
|
980
|
+
// 获取缓冲的日志
|
|
981
|
+
const logs = bufferLogger.getLogs();
|
|
982
|
+
const errorLogs = bufferLogger.getLogsByLevel('error');
|
|
210
983
|
```
|
|
211
984
|
|
|
212
|
-
##
|
|
985
|
+
## Token 自动刷新
|
|
213
986
|
|
|
214
987
|
```typescript
|
|
215
|
-
|
|
988
|
+
import {
|
|
989
|
+
createFetchClient,
|
|
990
|
+
createTokenRefreshInterceptor,
|
|
991
|
+
} from '@djvlc/openapi-client-core';
|
|
992
|
+
|
|
993
|
+
// 创建客户端时配置 Token 刷新
|
|
994
|
+
const client = createFetchClient({
|
|
216
995
|
baseUrl: 'https://api.example.com',
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// 修改请求
|
|
221
|
-
context.init.headers = new Headers(context.init.headers);
|
|
222
|
-
context.init.headers.set('X-Custom', 'value');
|
|
223
|
-
return context;
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
response: [
|
|
227
|
-
async (response, context) => {
|
|
228
|
-
// 处理响应
|
|
229
|
-
if (response.status === 401) {
|
|
230
|
-
// Token 刷新逻辑
|
|
231
|
-
const newToken = await refreshToken();
|
|
232
|
-
// 重试请求...
|
|
233
|
-
}
|
|
234
|
-
return response;
|
|
235
|
-
},
|
|
236
|
-
],
|
|
237
|
-
error: [
|
|
238
|
-
(error, context) => {
|
|
239
|
-
// 统一错误上报
|
|
240
|
-
reportError(error);
|
|
241
|
-
throw error;
|
|
242
|
-
},
|
|
243
|
-
],
|
|
996
|
+
auth: {
|
|
997
|
+
type: 'bearer',
|
|
998
|
+
getToken: () => tokenService.getAccessToken(),
|
|
244
999
|
},
|
|
245
1000
|
});
|
|
1001
|
+
|
|
1002
|
+
// 添加 Token 刷新拦截器
|
|
1003
|
+
client.interceptors.addErrorInterceptor(
|
|
1004
|
+
createTokenRefreshInterceptor({
|
|
1005
|
+
refreshToken: async () => {
|
|
1006
|
+
const newToken = await authService.refresh();
|
|
1007
|
+
tokenService.setAccessToken(newToken);
|
|
1008
|
+
return newToken;
|
|
1009
|
+
},
|
|
1010
|
+
triggerStatusCodes: [401],
|
|
1011
|
+
maxRetries: 1,
|
|
1012
|
+
onTokenRefreshed: (newToken) => {
|
|
1013
|
+
console.log('Token 已刷新');
|
|
1014
|
+
},
|
|
1015
|
+
onRefreshFailed: (error) => {
|
|
1016
|
+
console.log('Token 刷新失败,跳转登录');
|
|
1017
|
+
redirectToLogin();
|
|
1018
|
+
},
|
|
1019
|
+
executeRequest: (context) => client.request(context.originalOptions),
|
|
1020
|
+
})
|
|
1021
|
+
);
|
|
246
1022
|
```
|
|
247
1023
|
|
|
248
|
-
##
|
|
1024
|
+
## 工具函数
|
|
249
1025
|
|
|
250
1026
|
```typescript
|
|
251
|
-
import {
|
|
1027
|
+
import {
|
|
1028
|
+
// 请求 ID
|
|
1029
|
+
generateRequestId,
|
|
1030
|
+
generateTraceId,
|
|
1031
|
+
isValidRequestId,
|
|
1032
|
+
|
|
1033
|
+
// 重试延迟
|
|
1034
|
+
calculateRetryDelay,
|
|
1035
|
+
parseRetryAfter,
|
|
1036
|
+
|
|
1037
|
+
// 延迟执行
|
|
1038
|
+
sleep,
|
|
1039
|
+
sleepWithAbort,
|
|
1040
|
+
|
|
1041
|
+
// URL 处理
|
|
1042
|
+
buildUrl,
|
|
1043
|
+
extractPath,
|
|
1044
|
+
parseQueryString,
|
|
1045
|
+
joinPaths,
|
|
1046
|
+
|
|
1047
|
+
// 请求头处理
|
|
1048
|
+
mergeHeaders,
|
|
1049
|
+
getHeader,
|
|
1050
|
+
setHeader,
|
|
1051
|
+
removeHeader,
|
|
1052
|
+
} from '@djvlc/openapi-client-core';
|
|
252
1053
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
},
|
|
265
|
-
|
|
1054
|
+
// 生成 ID
|
|
1055
|
+
const requestId = generateRequestId(); // req_m5abc123_k7def456
|
|
1056
|
+
const traceId = generateTraceId(); // trace_m5abc123_k7def456_x8ghi789
|
|
1057
|
+
|
|
1058
|
+
// URL 处理
|
|
1059
|
+
const url = buildUrl('https://api.example.com', '/users', { page: 1, size: 10 });
|
|
1060
|
+
// https://api.example.com/users?page=1&size=10
|
|
1061
|
+
|
|
1062
|
+
// 请求头处理
|
|
1063
|
+
const headers = mergeHeaders(
|
|
1064
|
+
{ 'Content-Type': 'application/json' },
|
|
1065
|
+
{ 'Authorization': 'Bearer xxx' },
|
|
1066
|
+
);
|
|
266
1067
|
```
|
|
267
1068
|
|
|
268
|
-
## API
|
|
1069
|
+
## API 参考
|
|
269
1070
|
|
|
270
1071
|
### 错误类
|
|
271
1072
|
|
|
272
1073
|
| 类 | 说明 |
|
|
273
1074
|
|---|---|
|
|
274
|
-
| `
|
|
1075
|
+
| `BaseClientError` | 所有错误的基类 |
|
|
1076
|
+
| `ApiError` | 业务错误(HTTP 非 2xx 响应) |
|
|
275
1077
|
| `NetworkError` | 网络层错误 |
|
|
276
1078
|
| `TimeoutError` | 超时错误 |
|
|
277
1079
|
| `AbortError` | 请求被取消 |
|
|
278
1080
|
|
|
279
|
-
###
|
|
1081
|
+
### 认证器
|
|
1082
|
+
|
|
1083
|
+
| 类 | 说明 |
|
|
1084
|
+
|---|---|
|
|
1085
|
+
| `BearerAuthenticator` | Bearer Token 认证 |
|
|
1086
|
+
| `ApiKeyAuthenticator` | API Key 认证 |
|
|
1087
|
+
| `BasicAuthenticator` | Basic Auth 认证 |
|
|
1088
|
+
| `CustomAuthenticator` | 自定义认证 |
|
|
1089
|
+
| `NoAuthenticator` | 无认证 |
|
|
1090
|
+
|
|
1091
|
+
### 拦截器
|
|
1092
|
+
|
|
1093
|
+
| 类 | 类型 | 说明 |
|
|
1094
|
+
|---|---|---|
|
|
1095
|
+
| `AuthInterceptor` | 请求 | 自动添加认证信息 |
|
|
1096
|
+
| `RequestIdInterceptor` | 请求 | 生成请求 ID |
|
|
1097
|
+
| `TraceInterceptor` | 请求 | 添加追踪上下文 |
|
|
1098
|
+
| `TimeoutInterceptor` | 请求 | 智能设置超时 |
|
|
1099
|
+
| `ErrorTransformInterceptor` | 响应 | 错误格式转换 |
|
|
1100
|
+
| `RetryInterceptor` | 错误 | 自动重试 |
|
|
1101
|
+
| `TokenRefreshInterceptor` | 错误 | Token 自动刷新 |
|
|
1102
|
+
| `LoggingInterceptor` | 错误 | 错误日志记录 |
|
|
1103
|
+
| `ReportingInterceptor` | 错误 | 错误上报监控 |
|
|
1104
|
+
|
|
1105
|
+
### 客户端
|
|
280
1106
|
|
|
281
|
-
|
|
|
1107
|
+
| 类/函数 | 说明 |
|
|
282
1108
|
|---|---|
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
|
291
|
-
|
|
292
|
-
### 类型
|
|
293
|
-
|
|
294
|
-
| 类型 | 说明 |
|
|
1109
|
+
| `FetchClient` | 基于原生 fetch 的独立客户端 |
|
|
1110
|
+
| `createFetchClient()` | 创建 FetchClient 实例 |
|
|
1111
|
+
| `createEnhancedFetch()` | 创建增强的 fetch 函数(用于 typescript-fetch 生成代码) |
|
|
1112
|
+
| `createAxiosInstance()` | 创建配置好的 Axios 实例(向后兼容) |
|
|
1113
|
+
|
|
1114
|
+
### 插件
|
|
1115
|
+
|
|
1116
|
+
| 类/函数 | 说明 |
|
|
295
1117
|
|---|---|
|
|
296
|
-
| `
|
|
297
|
-
| `
|
|
298
|
-
| `
|
|
299
|
-
| `
|
|
300
|
-
| `Interceptors` | 拦截器配置 |
|
|
301
|
-
| `RequestMetrics` | 请求指标 |
|
|
302
|
-
| `MetricsSummary` | 指标摘要 |
|
|
303
|
-
| `MetricsCollector` | 指标收集器接口 |
|
|
1118
|
+
| `RequestDeduper` | 请求去重器 |
|
|
1119
|
+
| `DefaultMetricsCollector` | 默认指标收集器 |
|
|
1120
|
+
| `ConsoleLogger` | 控制台日志器 |
|
|
1121
|
+
| `BufferLogger` | 缓冲日志器(测试用) |
|
|
304
1122
|
|
|
305
1123
|
## 版本信息
|
|
306
1124
|
|
|
307
|
-
SDK 版本在构建时自动注入:
|
|
308
|
-
|
|
309
1125
|
```typescript
|
|
310
|
-
import {
|
|
1126
|
+
import { VERSION, SDK_NAME, getSdkInfo } from '@djvlc/openapi-client-core';
|
|
311
1127
|
|
|
312
|
-
console.log(SDK_NAME
|
|
313
|
-
//
|
|
1128
|
+
console.log(`${SDK_NAME}@${VERSION}`);
|
|
1129
|
+
// @djvlc/openapi-client-core@2.0.0
|
|
314
1130
|
|
|
315
1131
|
const info = getSdkInfo();
|
|
316
|
-
|
|
317
|
-
// 输出: { name: '@djvlc/openapi-client-core', version: '1.0.0' }
|
|
1132
|
+
// { name: '@djvlc/openapi-client-core', version: '2.0.0' }
|
|
318
1133
|
```
|
|
1134
|
+
|
|
1135
|
+
## 迁移指南(从 1.x 升级)
|
|
1136
|
+
|
|
1137
|
+
### 破坏性变更
|
|
1138
|
+
|
|
1139
|
+
1. **模块路径变更**:原来的单文件被拆分为多个模块目录
|
|
1140
|
+
2. **类型定义变更**:部分类型名称和结构有调整
|
|
1141
|
+
3. **HttpClient 重命名**:`HttpClient` 改名为 `FetchClient`
|
|
1142
|
+
4. **createClient 重命名**:`createClient` 改名为 `createFetchClient`
|
|
1143
|
+
5. **默认使用 Fetch**:OpenAPI 生成的客户端默认使用 `typescript-fetch`,不再依赖 axios
|
|
1144
|
+
|
|
1145
|
+
### 从 axios 迁移到 fetch
|
|
1146
|
+
|
|
1147
|
+
如果你之前使用 `createAxiosInstance`,现在推荐使用 `createEnhancedFetch`:
|
|
1148
|
+
|
|
1149
|
+
```typescript
|
|
1150
|
+
// 旧代码(axios)
|
|
1151
|
+
import { createAxiosInstance } from '@djvlc/openapi-client-core';
|
|
1152
|
+
const axiosInstance = createAxiosInstance({ baseUrl: '/api' });
|
|
1153
|
+
const api = new PagesApi(config, '/api', axiosInstance);
|
|
1154
|
+
|
|
1155
|
+
// 新代码(fetch)
|
|
1156
|
+
import { createEnhancedFetch } from '@djvlc/openapi-client-core';
|
|
1157
|
+
const enhancedFetch = createEnhancedFetch({ baseUrl: '/api' });
|
|
1158
|
+
const api = new PagesApi(config, '/api', enhancedFetch);
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
### 向后兼容
|
|
1162
|
+
|
|
1163
|
+
- `createAxiosInstance` 仍然可用,用于需要 axios 的场景
|
|
1164
|
+
- 所有配置选项在 `createEnhancedFetch` 中保持一致
|
|
1165
|
+
|
|
1166
|
+
## License
|
|
1167
|
+
|
|
1168
|
+
MIT
|