soon-fetch 4.0.0-beta.5 → 4.0.1

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
@@ -36,9 +36,9 @@
36
36
  import { createSoon, soonFetch } from "soon-fetch";
37
37
 
38
38
  // 使用 soonFetch 作为基础请求函数
39
- const request = async <T>(url: string, options?: SoonOptions) => {
39
+ const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
40
40
  const isGet = !options?.method || options?.method.toLocaleLowerCase() === "get";
41
- const response = await soonFetch<T>({
41
+ const response = await soonFetch({
42
42
  url,
43
43
  options,
44
44
  baseURL: '/api',
@@ -51,7 +51,12 @@ const request = async <T>(url: string, options?: SoonOptions) => {
51
51
  staleTime: isGet ? 2 * 1000 : 0,
52
52
  },
53
53
  });
54
- return response;
54
+
55
+ if (!response.ok) {
56
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
57
+ }
58
+
59
+ return response.json() as Promise<T>;
55
60
  };
56
61
 
57
62
  const soon = createSoon(request);
@@ -133,7 +138,7 @@ import { useEffect, useRef, useState } from "react";
133
138
  type User = { name: string; job: string };
134
139
  const api = soon.GET("/api/users").Query<{ page: number }>().Ok<User[]>();
135
140
  export default function App() {
136
- const refAbort = useRef([]);
141
+ const refAbort = useRef<[AbortController] | []>([]);
137
142
  const [list, setList] = useState<User[]>([]);
138
143
  const [page, setPage] = useState(1);
139
144
  useEffect(() => {
@@ -156,13 +161,15 @@ export default function App() {
156
161
 
157
162
  ##### Rapid Define APIs
158
163
 
159
- ```typescript
160
- //can be GET POST PATCH PUT DELETE
164
+ ```ts
165
+ //可以是 GET POST PATCH PUT DELETE
166
+ //GET 请求数据传递至query,其他方法请求数据传递至body
161
167
  soon.GET(url:string).Query<Query>().Ok<Response>()
162
168
  soon.POST(url:string).Body<Body>().Ok<Response>()
163
169
  soon.GET(url:string).Options({ timeout: 5000 }).Ok<Response>()
164
170
  soon.POST(url:string).Body<Body>().Options({ timeout: 5000 }).Ok<Response>()
165
- //define an api
171
+
172
+ //define an api
166
173
  export const getUserInfo = soon.GET("/user/:id").Ok();
167
174
  //then use in any where
168
175
  getUserInfo({ id: 2 }).then((res) => console.log(res));
@@ -180,6 +187,10 @@ export const login = soon
180
187
  login({ username: "admin", password: "123" }).then((res) => {
181
188
  localStorage.setItem("token", res.token);
182
189
  });
190
+
191
+ //with Params for type safety
192
+ export const getUserById = soon.GET("/user/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
193
+ getUserById({ id: 1 }).then((res) => console.log(res));
183
194
  ```
184
195
 
185
196
  ### API
@@ -223,25 +234,53 @@ Create a soon request instance.
223
234
  **Example:**
224
235
 
225
236
  ```typescript
226
- const soon = createSoon(
227
- async <T>(url: string, options?: SoonOptions): Promise<T> => {
228
- const response = await fetch(url, options);
229
- return response.json();
237
+ import { createSoon, soonFetch } from "soon-fetch";
238
+
239
+ // Define custom request wrapper with business logic
240
+ const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
241
+ const isGet = !options?.method || options?.method.toLowerCase() === "get";
242
+
243
+ const response = await soonFetch({
244
+ url,
245
+ options,
246
+ baseURL: '/api',
247
+ baseOptions: {
248
+ timeout: 20 * 1000,
249
+ headers: new Headers({
250
+ Authorization: "Bearer " + localStorage.getItem("token"),
251
+ }),
252
+ share: isGet,
253
+ staleTime: isGet ? 2 * 1000 : 0,
254
+ },
255
+ });
256
+
257
+ if (!response.ok) {
258
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
230
259
  }
231
- );
260
+
261
+ return response.json() as Promise<T>;
262
+ };
232
263
 
233
- // Usage example
234
- const data = await soon.get<{ id: number; name: string }[]> ("/api/users");
264
+ // Create soon instance
265
+ const soon = createSoon(request);
235
266
 
236
- // Define API with options
237
- export const login = soon
238
- .POST("/user/login")
239
- .Body<{ username: string; password: string }>()
240
- .Ok<{ token: string }>();
267
+ // Usage 1: Shortcut methods with generics
268
+ const users = await soon.get<{ id: number; name: string }[]>("/api/users");
241
269
 
242
- login({ username: "admin", password: "123" }).then((res) => {
243
- localStorage.setItem("token", res.token);
244
- });
270
+ // Usage 2: Define typed APIs with chain calls
271
+ export const getUserById = soon
272
+ .GET("/user/:id")
273
+ .Params<{ id: number }>()
274
+ .Ok<{ id: number; name: string }>();
275
+
276
+ export const createUser = soon
277
+ .POST("/user")
278
+ .Body<{ name: string; email: string }>()
279
+ .Ok<{ id: number }>();
280
+
281
+ // Usage 3: Use defined APIs
282
+ const user = await getUserById({ id: 1 });
283
+ const newUser = await createUser({ name: "John", email: "john@example.com" });
245
284
  ```
246
285
 
247
286
  #### createShortApi
@@ -270,6 +309,10 @@ const API = createShortApi(
270
309
  // Usage example
271
310
  const getUser = API.GET("/api/users/:id").Ok<{ id: number; name: string }>();
272
311
  const userData = await getUser({ id: 1 });
312
+
313
+ // With Params for type safety
314
+ const getUserById = API.GET("/api/users/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
315
+ const userData = await getUserById({ id: 1 });
273
316
  ```
274
317
 
275
318
  #### createShortMethods
@@ -295,7 +338,7 @@ const methods = createShortMethods(["get", "post"] as const, (method) => {
295
338
  // Usage: methods.get<{ id: number; name: string }[]>('/api/users')
296
339
  ```
297
340
 
298
- #### parseUrlOptions
341
+ #### parseOptions
299
342
 
300
343
  Parse URL options.
301
344
 
@@ -303,17 +346,17 @@ Parse URL options.
303
346
 
304
347
  - `urlOptions`: Object containing url, options, baseURL and baseOptions
305
348
 
306
- **Returns:** Tuple of processed url and options
349
+ **Returns:** Object containing parsed url, options, is_body_json, and abortController
307
350
 
308
351
  **Example:**
309
352
 
310
353
  ```typescript
311
- const [url, options] = parseUrlOptions({
354
+ const parsed = parseOptions({
312
355
  url: "/api/users/:id",
313
356
  options: { params: { id: "123" } },
314
357
  baseURL: "https://api.example.com",
315
358
  });
316
- // Returns: ['https://api.example.com/api/users/123', options]
359
+ // Returns: { url: 'https://api.example.com/api/users/123', options: {...}, is_body_json: false, abortController: AbortController }
317
360
  ```
318
361
 
319
362
  #### mergeHeaders
@@ -356,7 +399,7 @@ const mergedSignal = mergeSignals(
356
399
  );
357
400
  ```
358
401
 
359
- #### mergeUrl
402
+ #### parseUrl
360
403
 
361
404
  Merge URL and its related parameters.
362
405
  Handle baseURL, path parameters and query parameters to generate complete URL.
@@ -443,24 +486,6 @@ const key = genRequestKey({
443
486
  });
444
487
  ```
445
488
 
446
- #### raceAbort
447
-
448
- Race condition handling function.
449
- Used to handle request race conditions, terminating previous requests.
450
-
451
- **Parameters:**
452
-
453
- - `abortController`: Controller of the current request
454
- - `controllers`: Array of existing controllers
455
-
456
- **Example:**
457
-
458
- ```typescript
459
- const controller = new AbortController();
460
- const controllers: AbortController[] = [];
461
- // 注意:raceAbort 函数已不再直接导出,而是通过 createRequestStore 使用
462
- ```
463
-
464
489
  #### createRequestStore
465
490
 
466
491
  Create request store instance.
@@ -551,12 +576,12 @@ A lightweight fetch wrapper with caching, sharing, and race condition handling.
551
576
  - `store`: Custom request store
552
577
  - `sortRequestKey`: Whether to sort request key
553
578
 
554
- **Returns:** Promise that resolves to the response
579
+ **Returns:** Promise that resolves to Response
555
580
 
556
581
  **Example:**
557
582
 
558
583
  ```typescript
559
- const data = await soonFetch<User[]>({
584
+ const response = await soonFetch({
560
585
  url: "/api/users",
561
586
  options: {
562
587
  method: "GET",
@@ -566,27 +591,12 @@ const data = await soonFetch<User[]>({
566
591
  },
567
592
  baseURL: "https://api.example.com",
568
593
  });
569
- ```
570
-
571
- #### parseWithBase
572
-
573
- Parse base URL configuration.
574
- Process baseURL, headers, body and other configuration items to generate final request configuration.
575
-
576
- **Parameters:**
577
-
578
- - `urlOptions`: Object containing url, options, baseURL and baseOptions
579
594
 
580
- **Returns:** Processed url, options, is_body_json and abortController
581
-
582
- **Example:**
595
+ if (!response.ok) {
596
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
597
+ }
583
598
 
584
- ```typescript
585
- const result = parseWithBase({
586
- url: "/api/users",
587
- options: { method: "GET" },
588
- baseURL: "https://api.example.com",
589
- });
599
+ const data = await response.json() as User[];
590
600
  ```
591
601
 
592
602
  #### toFormData
@@ -653,28 +663,6 @@ const buffer = await progressReadBody(response.body!, (progress, downloaded, tot
653
663
  });
654
664
  ```
655
665
 
656
- #### parseOptions
657
-
658
- Parse and merge request options.
659
- Used to process request options, including baseURL, headers, and body.
660
-
661
- **Parameters:**
662
-
663
- - `urlOptions`: Object containing url, options, baseURL, and baseOptions
664
-
665
- **Returns:** Object containing parsed url, options, is_body_json, and abortController
666
-
667
- **Example:**
668
-
669
- ```typescript
670
- const parsed = parseOptions({
671
- url: "/api/users",
672
- options: { method: "GET", query: { page: 1 } },
673
- baseURL: "https://api.example.com",
674
- baseOptions: { timeout: 5000 },
675
- });
676
- ```
677
-
678
666
  #### requestWithStore
679
667
 
680
668
  Request wrapper with store support.
@@ -741,9 +729,9 @@ const data = await requestWithStore(store, () => fetch(url, options), requestKey
741
729
  import { createSoon, soonFetch } from "soon-fetch";
742
730
 
743
731
  // 使用 soonFetch 作为基础请求函数
744
- const request = async <T>(url: string, options?: SoonOptions) => {
732
+ const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
745
733
  const isGet = !options?.method || options?.method.toLocaleLowerCase() === "get";
746
- const response = await soonFetch<T>({
734
+ const response = await soonFetch({
747
735
  url,
748
736
  options,
749
737
  baseURL: '/api',
@@ -756,7 +744,12 @@ const request = async <T>(url: string, options?: SoonOptions) => {
756
744
  staleTime: isGet ? 2 * 1000 : 0,
757
745
  },
758
746
  });
759
- return response;
747
+
748
+ if (!response.ok) {
749
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
750
+ }
751
+
752
+ return response.json() as Promise<T>;
760
753
  };
761
754
 
762
755
  const soon = createSoon(request);
@@ -779,6 +772,10 @@ export const login = soon
779
772
  login({ username: "admin", password: "123" }).then((res) => {
780
773
  localStorage.setItem("token", res.token);
781
774
  });
775
+
776
+ //使用 Params 进行类型安全定义
777
+ export const getUserById = soon.GET("/user/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
778
+ getUserById({ id: 1 }).then((res) => console.log(res));
782
779
  ```
783
780
 
784
781
  ### 特别功能
@@ -839,7 +836,7 @@ import { useEffect, useRef, useState } from "react";
839
836
  type User = { name: string; job: string };
840
837
  const api = soon.GET("/api/users").Query<{ page: number }>().Ok<User[]>();
841
838
  export default function App() {
842
- const refAbort = useRef([]);
839
+ const refAbort = useRef<[AbortController] | []>([]);
843
840
  const [list, setList] = useState<User[]>([]);
844
841
  const [page, setPage] = useState(1);
845
842
  useEffect(() => {
@@ -888,6 +885,10 @@ export default function App() {
888
885
  login({username:'admin',password:'123'}).then(res=>{
889
886
  localStorage.setItem('token', res.token);
890
887
  })
888
+
889
+ //使用 Params 进行类型安全定义
890
+ export const getUserById = soon.GET('/user/:id').Params<{ id: number }>().Ok<{ id: number; name: string }>();
891
+ getUserById({ id: 1 }).then(res => console.log(res));
891
892
  ```
892
893
 
893
894
  ### API
@@ -969,7 +970,7 @@ login({ username: "admin", password: "123" }).then((res) => {
969
970
  const API = createShortApi(
970
971
  async (url, method, params, query, body, options) => {
971
972
  // 处理请求逻辑
972
- const _url = mergeUrl(url, { params, query });
973
+ const { url: _url } = parseUrl(url, { params, query });
973
974
  const response = await fetch(_url, { ...options, method, body });
974
975
  return response.json();
975
976
  }
@@ -978,6 +979,10 @@ const API = createShortApi(
978
979
  // 使用示例
979
980
  const getUser = API.GET("/api/users/:id").Ok();
980
981
  const userData = await getUser({ id: 1 });
982
+
983
+ // 使用 Params 进行类型安全定义
984
+ const getUserById = API.GET("/api/users/:id").Params<{ id: number }>().Ok<{ id: number; name: string }>();
985
+ const userData = await getUserById({ id: 1 });
981
986
  ```
982
987
 
983
988
  #### createShortMethods
@@ -1000,7 +1005,7 @@ const methods = createShortMethods(["get", "post"] as const, (method) => {
1000
1005
  // 使用: methods.get('/api/users')
1001
1006
  ```
1002
1007
 
1003
- #### parseUrlOptions
1008
+ #### parseOptions
1004
1009
 
1005
1010
  解析 URL 选项。
1006
1011
 
@@ -1008,17 +1013,17 @@ const methods = createShortMethods(["get", "post"] as const, (method) => {
1008
1013
 
1009
1014
  - `urlOptions`: 包含 url、options、baseURL 和 baseOptions 的对象
1010
1015
 
1011
- **返回:** 处理后的 url 和 options 元组
1016
+ **返回:** 包含解析后的 url、options、is_body_jsonabortController 的对象
1012
1017
 
1013
1018
  **示例:**
1014
1019
 
1015
1020
  ```typescript
1016
- const [url, options] = parseUrlOptions({
1021
+ const parsed = parseOptions({
1017
1022
  url: "/api/users/:id",
1018
1023
  options: { params: { id: "123" } },
1019
1024
  baseURL: "https://api.example.com",
1020
1025
  });
1021
- // 返回: ['https://api.example.com/api/users/123', options]
1026
+ // 返回: { url: 'https://api.example.com/api/users/123', options: {...}, is_body_json: false, abortController: AbortController }
1022
1027
  ```
1023
1028
 
1024
1029
  #### mergeHeaders
@@ -1061,7 +1066,7 @@ const mergedSignal = mergeSignals(
1061
1066
  );
1062
1067
  ```
1063
1068
 
1064
- #### mergeUrl
1069
+ #### parseUrl
1065
1070
 
1066
1071
  合并 URL 及其相关参数。
1067
1072
  处理 baseURL、路径参数和查询参数,生成完整 URL。
@@ -1076,7 +1081,7 @@ const mergedSignal = mergeSignals(
1076
1081
  **示例:**
1077
1082
 
1078
1083
  ```typescript
1079
- const url = mergeUrl("/api/users/:id", {
1084
+ const url = parseUrl("/api/users/:id", {
1080
1085
  params: { id: "123" },
1081
1086
  query: { filter: "active" },
1082
1087
  baseURL: "https://api.example.com",
@@ -1148,24 +1153,6 @@ const key = genRequestKey({
1148
1153
  });
1149
1154
  ```
1150
1155
 
1151
- #### raceAbort
1152
-
1153
- 竞态处理函数。
1154
- 用于处理请求竞态,终止之前的请求。
1155
-
1156
- **参数:**
1157
-
1158
- - `abortController`: 当前请求的控制器
1159
- - `controllers`: 已存在的控制器数组
1160
-
1161
- **示例:**
1162
-
1163
- ```typescript
1164
- const controller = new AbortController();
1165
- const controllers = [];
1166
- raceAbort(controller, controllers); // 终止之前的请求并添加当前控制器
1167
- ```
1168
-
1169
1156
  #### createRequestStore
1170
1157
 
1171
1158
  创建请求存储实例。
@@ -1256,12 +1243,12 @@ silentRefresh(
1256
1243
  - `store`: 自定义请求存储
1257
1244
  - `sortRequestKey`: 是否对请求键进行排序
1258
1245
 
1259
- **返回:** 解析为响应的 Promise
1246
+ **返回:** 解析为 Response 的 Promise
1260
1247
 
1261
1248
  **示例:**
1262
1249
 
1263
1250
  ```typescript
1264
- const data = await soonFetch<User[]>({
1251
+ const response = await soonFetch({
1265
1252
  url: "/api/users",
1266
1253
  options: {
1267
1254
  method: "GET",
@@ -1271,27 +1258,12 @@ const data = await soonFetch<User[]>({
1271
1258
  },
1272
1259
  baseURL: "https://api.example.com",
1273
1260
  });
1274
- ```
1275
1261
 
1276
- #### parseWithBase
1277
-
1278
- 解析基础 URL 配置。
1279
- 处理 baseURL、headers、body 等配置项,生成最终的请求配置。
1280
-
1281
- **参数:**
1282
-
1283
- - `urlOptions`: 包含 url、options、baseURL 和 baseOptions 的对象
1284
-
1285
- **返回:** 处理后的 url、options、is_body_json 和 abortController
1286
-
1287
- **示例:**
1262
+ if (!response.ok) {
1263
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1264
+ }
1288
1265
 
1289
- ```typescript
1290
- const result = parseWithBase({
1291
- url: "/api/users",
1292
- options: { method: "GET" },
1293
- baseURL: "https://api.example.com",
1294
- });
1266
+ const data = await response.json() as User[];
1295
1267
  ```
1296
1268
 
1297
1269
  #### toFormData
@@ -1358,28 +1330,6 @@ const buffer = await progressReadBody(response.body, (progress, downloaded, tota
1358
1330
  });
1359
1331
  ```
1360
1332
 
1361
- #### parseOptions
1362
-
1363
- 解析和合并请求选项。
1364
- 用于处理请求选项,包括 baseURL、headers 和 body。
1365
-
1366
- **参数:**
1367
-
1368
- - `urlOptions`: 包含 url、options、baseURL 和 baseOptions 的对象
1369
-
1370
- **返回:** 包含解析后的 url、options、is_body_json 和 abortController 的对象
1371
-
1372
- **示例:**
1373
-
1374
- ```typescript
1375
- const parsed = parseOptions({
1376
- url: "/api/users",
1377
- options: { method: "GET", query: { page: 1 } },
1378
- baseURL: "https://api.example.com",
1379
- baseOptions: { timeout: 5000 },
1380
- });
1381
- ```
1382
-
1383
1333
  #### requestWithStore
1384
1334
 
1385
1335
  带存储支持的请求包装器。
@@ -1412,6 +1362,253 @@ const data = await requestWithStore(store, () => fetch(url, options), requestKey
1412
1362
 
1413
1363
  ##### 安装 Installation
1414
1364
 
1415
- ```bash
1365
+ ```
1416
1366
  npm install soon-fetch
1417
1367
  ```
1368
+
1369
+ ## Best Practices / 最佳实践
1370
+
1371
+ #### Real-World Project Structure / 真实项目结构
1372
+
1373
+ Based on [soon-admin-vue](https://github.com/leafio/soon-admin-vue):
1374
+
1375
+ #### File Organization / 文件组织
1376
+
1377
+ ```
1378
+ src/api/
1379
+ ├── request.ts # Request wrapper with unified error handling
1380
+ ├── types.ts # Shared type definitions (e.g., PagedParams)
1381
+ ├── index.ts # Export all API modules
1382
+ └── modules/
1383
+ ├── auth.ts # Authentication APIs
1384
+ ├── user.ts # User management APIs
1385
+ ├── role.ts # Role management APIs
1386
+ ├── dept.ts # Department management APIs
1387
+ └── ... # Other domain-specific modules
1388
+ ```
1389
+
1390
+ #### Step 1: Create Request Wrapper (src/api/request.ts)
1391
+
1392
+ ```typescript
1393
+ import { createSoon, soonFetch } from "soon-fetch";
1394
+ import type { SoonOptions } from "soon-fetch";
1395
+
1396
+ type ReqOpts = SoonOptions & {
1397
+ retry?: { max?: number; enable?: (result: any) => boolean };
1398
+ toastErr?: boolean;
1399
+ };
1400
+
1401
+ const request = async <T>(url: string, options?: ReqOpts): Promise<T> => {
1402
+ const isGet = !options?.method || options?.method.toLowerCase() === "get";
1403
+
1404
+ try {
1405
+ const response = await soonFetch({
1406
+ url,
1407
+ options,
1408
+ baseURL: import.meta.env.VITE_API_BASE,
1409
+ baseOptions: {
1410
+ timeout: 20 * 1000,
1411
+ headers: new Headers({ Authorization: localStorage.getItem("token") ?? "" }),
1412
+ share: isGet,
1413
+ staleTime: isGet ? 2 * 1000 : 0,
1414
+ },
1415
+ });
1416
+
1417
+ if (!response.ok) {
1418
+ throw new Error(`HTTP ${response.status}`);
1419
+ }
1420
+
1421
+ // Auto-parse JSON
1422
+ if (response.headers.get("content-type")?.includes("json")) {
1423
+ const body = await response.json();
1424
+ return body.data as T;
1425
+ }
1426
+
1427
+ return response as unknown as T;
1428
+ } catch (error: any) {
1429
+ // Unified error handling
1430
+ if (error.name === "TimeoutError") {
1431
+ ElMessage.error("Request timeout");
1432
+ } else if (error.name !== "AbortError" && options?.toastErr !== false) {
1433
+ ElMessage.error(error.message);
1434
+ }
1435
+ throw error;
1436
+ }
1437
+ };
1438
+
1439
+ export const soon = createSoon<ReqOpts>(request);
1440
+ ```
1441
+
1442
+ **Key Points:**
1443
+ - ✅ **Centralized error handling**: All HTTP errors, timeouts, and business errors handled in one place
1444
+ - ✅ **Auto Token refresh**: Handle 401 errors and retry automatically
1445
+ - ✅ **Smart caching**: GET requests cached by default, write operations not cached
1446
+ - ✅ **No repetitive try-catch**: Components call APIs directly without error handling boilerplate
1447
+
1448
+ ---
1449
+
1450
+ #### Step 2: Define Typed APIs (src/api/modules/user.ts)
1451
+
1452
+ ```typescript
1453
+ import { downloadBlob, getHeaderFilename } from "soon-utils";
1454
+ import type { PagedParams } from "../types";
1455
+ import type { Dept } from "./dept";
1456
+ import type { Role } from "./role";
1457
+ import { soon } from "../request";
1458
+
1459
+ // Type definitions
1460
+ export type User = {
1461
+ id: number;
1462
+ username: string;
1463
+ email: string | null;
1464
+ phone: string | null;
1465
+ name: string | null;
1466
+ avatar: string | null;
1467
+ roleId: number | undefined;
1468
+ deptId: number | undefined;
1469
+ status: number;
1470
+ };
1471
+
1472
+ export type UserInfo = User & {
1473
+ createTime: Date;
1474
+ updateTime: Date | null;
1475
+ dept?: Pick<Dept, "id" | "name">;
1476
+ role?: Pick<Role, "id" | "name">;
1477
+ };
1478
+
1479
+ type ListQueryUser = PagedParams & {
1480
+ keyword?: string;
1481
+ timeRange?: [string, string];
1482
+ };
1483
+
1484
+ // CRUD APIs - simple and type-safe
1485
+ export const list_user = soon.GET("/user").Query<ListQueryUser>().Ok<{ list: UserInfo[] }>();
1486
+ export const add_user = soon.POST("/user").Body<User>().Ok();
1487
+ export const update_user = soon.PUT("/user/:id").Body<User>().Ok();
1488
+ export const del_user = soon.DELETE("/user/:id").Ok();
1489
+ export const detail_user = soon.GET("/user/:id").Ok<UserInfo>();
1490
+
1491
+ // File download - returns Response for custom handling
1492
+ export const download_user_table = async (query: ListQueryUser) => {
1493
+ return soon.get<Response>("/user/export", { query }).then(async (res) => {
1494
+ const body = await res.blob();
1495
+ const filename = getHeaderFilename(res.headers) ?? "user.xlsx";
1496
+ downloadBlob(body, filename);
1497
+ });
1498
+ };
1499
+
1500
+ // Captcha example
1501
+ export const getCaptcha = soon.GET("/captcha").Ok<{ id: number; img: string }>();
1502
+ ```
1503
+
1504
+ **Key Points:**
1505
+ - ✅ **Domain-based modules**: Each module handles one business domain (user, role, dept, etc.)
1506
+ - ✅ **Type safety**: Use `.Params<>()`, `.Body<>()`, `.Ok<>()` for full type inference
1507
+ - ✅ **Shared types**: Import cross-domain types from sibling modules
1508
+ - ✅ **Special operations**: Complex logic (file download) wrapped in async functions
1509
+ - ✅ **Naming convention**: Use snake_case for API exports (e.g., `list_user`, `add_user`)
1510
+
1511
+ ---
1512
+
1513
+ #### Step 3: Centralized Exports (src/api/index.ts)
1514
+
1515
+ ```typescript
1516
+ export * from "./modules/auth";
1517
+ export * from "./modules/user";
1518
+ export * from "./modules/role";
1519
+ // ... other modules
1520
+ ```
1521
+
1522
+ **Usage in components:**
1523
+ ```typescript
1524
+ import { list_user, add_user, del_user } from "@/api";
1525
+ ```
1526
+
1527
+ ---
1528
+
1529
+ #### Step 4: Use in Components - Clean and Simple!
1530
+
1531
+ **Vue 3 Example:**
1532
+ ```typescript
1533
+ <script setup lang="ts">
1534
+ import { ref, onMounted } from "vue";
1535
+ import { list_user, del_user } from "@/api";
1536
+ import type { UserInfo } from "@/api";
1537
+
1538
+ const users = ref<UserInfo[]>([]);
1539
+ const loading = ref(false);
1540
+
1541
+ const fetchUsers = async () => {
1542
+ loading.value = true;
1543
+ try {
1544
+ const { list } = await list_user({ page: 1, pageSize: 10 });
1545
+ users.value = list;
1546
+ } finally {
1547
+ loading.value = false;
1548
+ }
1549
+ };
1550
+
1551
+ const handleDelete = async (id: number) => {
1552
+ await del_user({ id });
1553
+ await fetchUsers();
1554
+ };
1555
+
1556
+ onMounted(fetchUsers);
1557
+ </script>
1558
+ ```
1559
+
1560
+ **React Example with Race Condition Handling:**
1561
+ ```typescript
1562
+ import { useEffect, useRef, useState } from "react";
1563
+ import { list_user } from "@/api";
1564
+ import type { UserInfo } from "@/api";
1565
+
1566
+ export default function UserList() {
1567
+ const [users, setUsers] = useState<UserInfo[]>([]);
1568
+ const [loading, setLoading] = useState(false);
1569
+ const abortRef = useRef<[AbortController] | []>([]);
1570
+
1571
+ useEffect(() => {
1572
+ setLoading(true);
1573
+ list_user({ page: 1, pageSize: 10 }, { aborts: abortRef.current })
1574
+ .then(({ list }) => setUsers(list))
1575
+ .catch((err) => {
1576
+ if (err.name !== "AbortError") console.error(err);
1577
+ })
1578
+ .finally(() => setLoading(false));
1579
+ }, []);
1580
+
1581
+ if (loading) return <div>Loading...</div>;
1582
+
1583
+ return (
1584
+ <ul>
1585
+ {users.map(u => (
1586
+ <li key={u.id}>{u.name}</li>
1587
+ ))}
1588
+ </ul>
1589
+ );
1590
+ }
1591
+ ```
1592
+
1593
+ **Key Points:**
1594
+ - ✅ **No repetitive error handling**: Errors already handled in request wrapper
1595
+ - ✅ **Race condition control**: Use `useRef` to manage `aborts` parameter
1596
+ - ✅ **Clean code**: Focus on business logic, not network error boilerplate
1597
+ - ✅ **Type safety**: Full TypeScript support with autocomplete and type checking
1598
+
1599
+ ---
1600
+
1601
+ ### Summary / 总结
1602
+
1603
+ **Architecture Benefits:**
1604
+ 1. **Separation of Concerns**: Network layer (request.ts) vs Business layer (modules/) vs UI layer (components)
1605
+ 2. **DRY Principle**: Error handling written once, used everywhere
1606
+ 3. **Type Safety**: End-to-end type inference from API definition to component usage
1607
+ 4. **Maintainability**: Domain-based modules make it easy to find and modify APIs
1608
+ 5. **Scalability**: Easy to add new domains by creating new module files
1609
+
1610
+ **Core Philosophy:**
1611
+ - 🎯 **Centralize common logic** in request wrapper (errors, retries, caching)
1612
+ - 🎯 **Keep API definitions simple** with chainable type-safe methods
1613
+ - 🎯 **Components focus on UI** without repetitive error handling code
1614
+ - 🎯 **Handle edge cases** only when needed (race conditions, special business logic)