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 +358 -161
- package/dist/index.d.ts +560 -0
- package/dist/index.js +1 -0
- package/dist/index.umd.js +1 -0
- package/package.json +9 -7
- package/dist/index.cjs +0 -1
- package/dist/index.d.cts +0 -480
- package/dist/index.d.mts +0 -480
- package/dist/index.iife.js +0 -1
- package/dist/index.mjs +0 -1
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
|
|
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
|
-
|
|
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
|
-
```
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
//
|
|
234
|
-
const
|
|
264
|
+
// Create soon instance
|
|
265
|
+
const soon = createSoon(request);
|
|
235
266
|
|
|
236
|
-
//
|
|
237
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
####
|
|
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:**
|
|
349
|
+
**Returns:** Object containing parsed url, options, is_body_json, and abortController
|
|
307
350
|
|
|
308
351
|
**Example:**
|
|
309
352
|
|
|
310
353
|
```typescript
|
|
311
|
-
const
|
|
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:
|
|
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
|
-
####
|
|
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
|
|
579
|
+
**Returns:** Promise that resolves to Response
|
|
555
580
|
|
|
556
581
|
**Example:**
|
|
557
582
|
|
|
558
583
|
```typescript
|
|
559
|
-
const
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
595
|
+
if (!response.ok) {
|
|
596
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
597
|
+
}
|
|
583
598
|
|
|
584
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
####
|
|
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
|
-
**返回:**
|
|
1016
|
+
**返回:** 包含解析后的 url、options、is_body_json 和 abortController 的对象
|
|
1012
1017
|
|
|
1013
1018
|
**示例:**
|
|
1014
1019
|
|
|
1015
1020
|
```typescript
|
|
1016
|
-
const
|
|
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
|
-
// 返回:
|
|
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
|
-
####
|
|
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 =
|
|
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
|
-
**返回:**
|
|
1246
|
+
**返回:** 解析为 Response 的 Promise
|
|
1260
1247
|
|
|
1261
1248
|
**示例:**
|
|
1262
1249
|
|
|
1263
1250
|
```typescript
|
|
1264
|
-
const
|
|
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
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
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)
|