soon-fetch 4.0.0 → 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));
@@ -227,25 +234,53 @@ Create a soon request instance.
227
234
  **Example:**
228
235
 
229
236
  ```typescript
230
- const soon = createSoon(
231
- async <T>(url: string, options?: SoonOptions): Promise<T> => {
232
- const response = await fetch(url, options);
233
- 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}`);
234
259
  }
235
- );
260
+
261
+ return response.json() as Promise<T>;
262
+ };
236
263
 
237
- // Usage example
238
- const data = await soon.get<{ id: number; name: string }[]> ("/api/users");
264
+ // Create soon instance
265
+ const soon = createSoon(request);
239
266
 
240
- // Define API with options
241
- export const login = soon
242
- .POST("/user/login")
243
- .Body<{ username: string; password: string }>()
244
- .Ok<{ token: string }>();
267
+ // Usage 1: Shortcut methods with generics
268
+ const users = await soon.get<{ id: number; name: string }[]>("/api/users");
245
269
 
246
- login({ username: "admin", password: "123" }).then((res) => {
247
- localStorage.setItem("token", res.token);
248
- });
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" });
249
284
  ```
250
285
 
251
286
  #### createShortApi
@@ -541,12 +576,12 @@ A lightweight fetch wrapper with caching, sharing, and race condition handling.
541
576
  - `store`: Custom request store
542
577
  - `sortRequestKey`: Whether to sort request key
543
578
 
544
- **Returns:** Promise that resolves to the response
579
+ **Returns:** Promise that resolves to Response
545
580
 
546
581
  **Example:**
547
582
 
548
583
  ```typescript
549
- const data = await soonFetch<User[]>({
584
+ const response = await soonFetch({
550
585
  url: "/api/users",
551
586
  options: {
552
587
  method: "GET",
@@ -556,6 +591,12 @@ const data = await soonFetch<User[]>({
556
591
  },
557
592
  baseURL: "https://api.example.com",
558
593
  });
594
+
595
+ if (!response.ok) {
596
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
597
+ }
598
+
599
+ const data = await response.json() as User[];
559
600
  ```
560
601
 
561
602
  #### toFormData
@@ -688,9 +729,9 @@ const data = await requestWithStore(store, () => fetch(url, options), requestKey
688
729
  import { createSoon, soonFetch } from "soon-fetch";
689
730
 
690
731
  // 使用 soonFetch 作为基础请求函数
691
- const request = async <T>(url: string, options?: SoonOptions) => {
732
+ const request = async <T>(url: string, options?: SoonOptions): Promise<T> => {
692
733
  const isGet = !options?.method || options?.method.toLocaleLowerCase() === "get";
693
- const response = await soonFetch<T>({
734
+ const response = await soonFetch({
694
735
  url,
695
736
  options,
696
737
  baseURL: '/api',
@@ -703,7 +744,12 @@ const request = async <T>(url: string, options?: SoonOptions) => {
703
744
  staleTime: isGet ? 2 * 1000 : 0,
704
745
  },
705
746
  });
706
- 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>;
707
753
  };
708
754
 
709
755
  const soon = createSoon(request);
@@ -790,7 +836,7 @@ import { useEffect, useRef, useState } from "react";
790
836
  type User = { name: string; job: string };
791
837
  const api = soon.GET("/api/users").Query<{ page: number }>().Ok<User[]>();
792
838
  export default function App() {
793
- const refAbort = useRef([]);
839
+ const refAbort = useRef<[AbortController] | []>([]);
794
840
  const [list, setList] = useState<User[]>([]);
795
841
  const [page, setPage] = useState(1);
796
842
  useEffect(() => {
@@ -1197,12 +1243,12 @@ silentRefresh(
1197
1243
  - `store`: 自定义请求存储
1198
1244
  - `sortRequestKey`: 是否对请求键进行排序
1199
1245
 
1200
- **返回:** 解析为响应的 Promise
1246
+ **返回:** 解析为 Response 的 Promise
1201
1247
 
1202
1248
  **示例:**
1203
1249
 
1204
1250
  ```typescript
1205
- const data = await soonFetch<User[]>({
1251
+ const response = await soonFetch({
1206
1252
  url: "/api/users",
1207
1253
  options: {
1208
1254
  method: "GET",
@@ -1212,6 +1258,12 @@ const data = await soonFetch<User[]>({
1212
1258
  },
1213
1259
  baseURL: "https://api.example.com",
1214
1260
  });
1261
+
1262
+ if (!response.ok) {
1263
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1264
+ }
1265
+
1266
+ const data = await response.json() as User[];
1215
1267
  ```
1216
1268
 
1217
1269
  #### toFormData
@@ -1310,6 +1362,253 @@ const data = await requestWithStore(store, () => fetch(url, options), requestKey
1310
1362
 
1311
1363
  ##### 安装 Installation
1312
1364
 
1313
- ```bash
1365
+ ```
1314
1366
  npm install soon-fetch
1315
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)