Skip to content

Commit d673e7b

Browse files
authored
feat(request): refactor Request to thenable handle with lifecycle callbacks (#57)
- RequestReturn → RequestHandle<T> (thenable + abort()) - Remove .promise — directly awaitable - Remove loading/error/data/aborted fields - Add onSuccess/onError/onAbort/onFinally callbacks - Add timeout, retry, cache, deduplication, download progress - Support Blob/URLSearchParams body, 204/205 response, binary blob - Add 19 new tests, 36 total request tests passing
1 parent 7cc3574 commit d673e7b

69 files changed

Lines changed: 12861 additions & 7090 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,121 @@ import { capitalize } from '@cc-heart/utils'
1818
capitalize('string') // String
1919
```
2020

21+
## Request — composable best practices
22+
23+
```ts
24+
import { Request } from '@cc-heart/utils'
25+
import type { RequestInterceptor } from '@cc-heart/utils'
26+
```
27+
28+
### Principle: small instances + composition
29+
30+
Prefer small focused instances over one instance with all interceptors. Combine them with factory functions:
31+
32+
```ts
33+
// ── Building blocks: interceptors are pure functions ──
34+
const addAuth: RequestInterceptor = (config) => ({
35+
...config,
36+
headers: { ...config.headers, Authorization: `Bearer ${getToken()}` }
37+
})
38+
39+
const addLang: RequestInterceptor = (config) => ({
40+
...config,
41+
headers: { ...config.headers, 'Accept-Language': 'zh-CN' }
42+
})
43+
44+
const handleError = (err: unknown) => {
45+
toast.error(err)
46+
return err
47+
}
48+
49+
// ── Compose: each instance handles one concern ──
50+
const authApi = new Request('https://api.example.com')
51+
authApi.useRequestInterceptor(addAuth)
52+
authApi.useRequestInterceptor(addLang)
53+
authApi.useErrorInterceptor(handleError)
54+
55+
const publicApi = new Request('https://open.api.com')
56+
57+
// ── Or use helper functions ──
58+
function withInterceptors(
59+
req: Request,
60+
interceptors: RequestInterceptor[]
61+
): Request {
62+
interceptors.forEach((i) => req.useRequestInterceptor(i))
63+
return req
64+
}
65+
function withBaseUrl(url: string): Request {
66+
return new Request(url)
67+
}
68+
69+
const api = withInterceptors(withBaseUrl('https://api.example.com'), [
70+
addAuth,
71+
addLang,
72+
])
73+
```
74+
75+
### Four calling styles
76+
77+
```ts
78+
const api = new Request('https://api.example.com')
79+
80+
// Style 1: async/await (recommended)
81+
try {
82+
const user = await api.get<User>('/users/1')
83+
setUser(user)
84+
} catch (e) {
85+
if ((e as Error).name === 'AbortError') return // user cancelled
86+
toast.error(e)
87+
}
88+
89+
// Style 2: lifecycle callbacks (React setState friendly)
90+
api.get('/users', {
91+
onSuccess: setUsers,
92+
onError: toast.error,
93+
onFinally: () => setLoading(false),
94+
})
95+
96+
// Style 3: promise chaining
97+
api.get<number>('/count')
98+
.then(n => n * 2)
99+
.then(setCount)
100+
.catch(toast.error)
101+
102+
// Style 4: mixed (await + callbacks, non-conflicting)
103+
const data = await api.get('/users', { onFinally: () => setLoading(false) })
104+
```
105+
106+
### Entity — group by domain
107+
108+
```ts
109+
// entities/user.ts
110+
const api = new Request('/api')
111+
112+
export const UserApi = {
113+
list: (page: number) =>
114+
api.get<User[]>('/users', { page }),
115+
get: (id: number) =>
116+
api.get<User>(`/users/${id}`),
117+
create: (data: CreateUserDto) =>
118+
api.post<User>('/users', data, { onSuccess: () => toast.success('created') }),
119+
}
120+
121+
// Usage
122+
const users = await UserApi.list(1)
123+
```
124+
125+
### Cache & dedup — isolated per instance
126+
127+
```ts
128+
const cachedApi = new Request('/api')
129+
// cache and dedup are instance-level, different Request instances are isolated
130+
const data1 = await cachedApi.get('/users', {}, { cache: { ttl: 5000 } })
131+
const data2 = await cachedApi.get('/users', {}, { cache: { ttl: 5000 } }) // cache hit
132+
133+
const otherApi = new Request('/api') // isolated cache
134+
```
135+
21136
## LICENSE
22137

23138
`@cc-heart/utils` is licensed under the [MIT License](./LICENSE).

README_ZH.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# @cc-heart/utils
2+
3+
[Docs](https://cc-hearts.github.io/utils/)
4+
5+
一个 JavaScript 工具库
6+
7+
## 安装
8+
9+
```shell
10+
npm install @cc-heart/utils
11+
```
12+
13+
## 使用
14+
15+
```js
16+
import { capitalize } from '@cc-heart/utils'
17+
18+
capitalize('string') // String
19+
```
20+
21+
## Request — 组合式最佳实践
22+
23+
```ts
24+
import { Request } from '@cc-heart/utils'
25+
import type { RequestInterceptor } from '@cc-heart/utils'
26+
```
27+
28+
### 原则:小实例 + 组合
29+
30+
不要一个实例挂全部拦截器,每个实例只做一件事,需要组合时用工厂函数包装:
31+
32+
```ts
33+
// ── 构建块:拦截器就是纯函数 ──
34+
const addAuth: RequestInterceptor = (config) => ({
35+
...config,
36+
headers: { ...config.headers, Authorization: `Bearer ${getToken()}` }
37+
})
38+
39+
const addLang: RequestInterceptor = (config) => ({
40+
...config,
41+
headers: { ...config.headers, 'Accept-Language': 'zh-CN' }
42+
})
43+
44+
const handleError = (err: unknown) => {
45+
toast.error(err)
46+
return err
47+
}
48+
49+
// ── 组合:每个实例只关注一个能力 ──
50+
const authApi = new Request('https://api.example.com')
51+
authApi.useRequestInterceptor(addAuth)
52+
authApi.useRequestInterceptor(addLang)
53+
authApi.useErrorInterceptor(handleError)
54+
55+
const publicApi = new Request('https://open.api.com')
56+
57+
// ── 或用辅助函数组合 ──
58+
function withInterceptors(
59+
req: Request,
60+
interceptors: RequestInterceptor[]
61+
): Request {
62+
interceptors.forEach((i) => req.useRequestInterceptor(i))
63+
return req
64+
}
65+
function withBaseUrl(url: string): Request {
66+
return new Request(url)
67+
}
68+
69+
const api = withInterceptors(withBaseUrl('https://api.example.com'), [
70+
addAuth,
71+
addLang,
72+
])
73+
```
74+
75+
### 四种调用风格
76+
77+
```ts
78+
const api = new Request('https://api.example.com')
79+
80+
// 风格 1:async/await(推荐)
81+
try {
82+
const user = await api.get<User>('/users/1')
83+
setUser(user)
84+
} catch (e) {
85+
if ((e as Error).name === 'AbortError') return // 用户主动取消
86+
toast.error(e)
87+
}
88+
89+
// 风格 2:生命周期回调(React setState 友好)
90+
api.get('/users', {
91+
onSuccess: setUsers,
92+
onError: toast.error,
93+
onFinally: () => setLoading(false),
94+
})
95+
96+
// 风格 3:Promise 链式
97+
api.get<number>('/count')
98+
.then(n => n * 2)
99+
.then(setCount)
100+
.catch(toast.error)
101+
102+
// 风格 4:混合使用(await + 回调,互不冲突)
103+
const data = await api.get('/users', { onFinally: () => setLoading(false) })
104+
```
105+
106+
### Entity —— 按实体聚合
107+
108+
```ts
109+
// entities/user.ts
110+
const api = new Request('/api')
111+
112+
export const UserApi = {
113+
list: (page: number) =>
114+
api.get<User[]>('/users', { page }),
115+
get: (id: number) =>
116+
api.get<User>(`/users/${id}`),
117+
create: (data: CreateUserDto) =>
118+
api.post<User>('/users', data, { onSuccess: () => toast.success('创建成功') }),
119+
}
120+
121+
// 使用
122+
const users = await UserApi.list(1)
123+
```
124+
125+
### 缓存 + 去重(按实例隔离)
126+
127+
```ts
128+
const cachedApi = new Request('/api')
129+
// cache 和 dedup 是实例级别的,不同的 Request 实例互相隔离
130+
const data1 = await cachedApi.get('/users', {}, { cache: { ttl: 5000 } })
131+
const data2 = await cachedApi.get('/users', {}, { cache: { ttl: 5000 } }) // 命中缓存
132+
133+
const otherApi = new Request('/api') // 独立缓存
134+
```
135+
136+
## 配置项速查
137+
138+
```ts
139+
interface RequestConfig {
140+
// 请求参数
141+
params?: Record<PropertyKey, any>
142+
data?: unknown
143+
144+
// 拦截器(单次请求)
145+
requestInterceptors?: RequestInterceptor[]
146+
responseInterceptors?: ResponseInterceptor[]
147+
errorInterceptors?: ErrorInterceptor[]
148+
149+
// 超时 & 重试
150+
timeout?: number // 毫秒,超时自动 abort 当前尝试
151+
retry?: number // 失败重试次数,0 = 不重试
152+
retryDelay?: number // 重试间隔(毫秒)
153+
154+
// 缓存(仅 GET)
155+
cache?: boolean | { ttl: number } // true = 默认 TTL 5s
156+
157+
// 下载进度
158+
onDownloadProgress?: (loaded: number, total: number) => void
159+
160+
// 生命周期回调
161+
onSuccess?: (data: unknown) => void
162+
onError?: (error: unknown) => void
163+
onAbort?: () => void
164+
onFinally?: () => void
165+
}
166+
```
167+
168+
## 返回类型
169+
170+
```ts
171+
interface RequestHandle<T> {
172+
// thenable,可直接 await
173+
then, catch, finally: Promise 方法
174+
// 取消请求
175+
abort: () => void
176+
}
177+
```
178+
179+
## LICENSE
180+
181+
`@cc-heart/utils` 基于 [MIT License](./LICENSE) 协议开源。

0 commit comments

Comments
 (0)