🔧 一个 JavaScript 通用工具库 —— 环境无关的核心层
utils 家族 · 核心:
@cc-heart/utils· Node.js 运行时:@cc-heart/utils-service· 浏览器:@cc-heart/utils-client
npm install @cc-heart/utilsimport { capitalize } from '@cc-heart/utils'
capitalize('string') // String三个小巧的异步控制原语。它们刻意保持可组合:用语义化的嵌套顺序表达意图,而不是把所有配置塞进一个函数。
import { retry, withTimeout, waitFor, TimeoutError } from '@cc-heart/utils'| 场景 | 函数 |
|---|---|
| 操作抛错了,值得再跑一次 | retry |
| 操作没有失败,只是结果还没准备好(比如 DOM 元素还没渲染) | waitFor |
| 任何操作想限制在 N 毫秒内 | withTimeout |
反复执行一个异步操作,直到成功或重试次数耗尽。全部失败时抛出最后一次的错误。
const user = await retry(() => fetchUser(id), { retries: 3 })
// 只重试服务端错误,并记录每次重试
const data = await retry(() => api.post(payload), {
retries: 5,
backoff: 'exponential',
shouldRetry: (err) => err instanceof HttpError && err.status >= 500,
onRetry: (err, attempt, nextDelay) =>
console.warn(`第 ${attempt} 次失败,${nextDelay}ms 后重试`)
})| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
retries |
number |
3 |
首次失败后的重试次数 |
delay |
number |
1000 |
重试基础间隔(ms) |
backoff |
'fixed' | 'linear' | 'exponential' |
'exponential' |
间隔增长策略 |
maxDelay |
number |
30000 |
计算出的间隔上限(ms) |
jitter |
boolean |
true |
间隔随机化(50%–100%),避免惊群 |
shouldRetry |
(error, attempt) => boolean |
— | 返回 false 立即抛出(如 4xx 错误) |
onRetry |
(error, attempt, nextDelay) => void |
— | 每次重试前触发,适合打日志 |
signal |
AbortSignal |
— | 取消重试间隔中的等待 |
让 Promise 和时间预算赛跑,超时则以 TimeoutError 拒绝。
const data = await withTimeout(fetch('/api/slow'), 5000)注意:超时拒绝并不会取消底层操作——如果操作本身支持取消,请把 AbortSignal 传给它。
轮询一个谓词(同步或异步),直到它返回 truthy 值并以该值兑现。特别适合 DOM 脚本:"元素还没渲染出来"不是错误,只是一个尚未满足的条件。
const modal = await waitFor(
() => document.querySelector<HTMLElement>('.modal'),
{ timeout: 10_000 }
)| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
interval |
number |
100 |
轮询间隔(ms) |
timeout |
number |
10000 |
整体时间预算(ms);传 Infinity 表示一直等 |
signal |
AbortSignal |
— | 取消轮询 |
// 每次尝试最多 5s,最多重试 3 次
await retry(() => withTimeout(fetchData(), 5000), { retries: 3 })
// 不断重试,但整个流程 30s 封顶
await withTimeout(retry(fetchData, { retries: 10 }), 30_000)
// 先等元素出现,再带重试地点击,全程 10s 预算
await withTimeout(
retry(
async () => {
const btn = await waitFor(() => document.querySelector('#submit'))
btn.click()
},
{ retries: 3 }
),
10_000
)用 instanceof TimeoutError 区分"超时失败"和"执行失败"——比如在 shouldRetry 里。
import { Request } from '@cc-heart/utils'
import type { RequestInterceptor } from '@cc-heart/utils'不要一个实例挂全部拦截器,每个实例只做一件事,需要组合时用工厂函数包装:
// ── 构建块:拦截器就是纯函数 ──
const addAuth: RequestInterceptor = (config) => ({
...config,
headers: { ...config.headers, Authorization: `Bearer ${getToken()}` }
})
const addLang: RequestInterceptor = (config) => ({
...config,
headers: { ...config.headers, 'Accept-Language': 'zh-CN' }
})
const handleError = (err: unknown) => {
toast.error(err)
return err
}
// ── 组合:每个实例只关注一个能力 ──
const authApi = new Request('https://api.example.com')
authApi.useRequestInterceptor(addAuth)
authApi.useRequestInterceptor(addLang)
authApi.useErrorInterceptor(handleError)
const publicApi = new Request('https://open.api.com')
// ── 或用辅助函数组合 ──
function withInterceptors(
req: Request,
interceptors: RequestInterceptor[]
): Request {
interceptors.forEach((i) => req.useRequestInterceptor(i))
return req
}
function withBaseUrl(url: string): Request {
return new Request(url)
}
const api = withInterceptors(withBaseUrl('https://api.example.com'), [
addAuth,
addLang,
])const api = new Request('https://api.example.com')
// 风格 1:async/await(推荐)
try {
const user = await api.get<User>('/users/1')
setUser(user)
} catch (e) {
if ((e as Error).name === 'AbortError') return // 用户主动取消
toast.error(e)
}
// 风格 2:生命周期回调(React setState 友好)
api.get('/users', {
onSuccess: setUsers,
onError: toast.error,
onFinally: () => setLoading(false),
})
// 风格 3:Promise 链式
api.get<number>('/count')
.then(n => n * 2)
.then(setCount)
.catch(toast.error)
// 风格 4:混合使用(await + 回调,互不冲突)
const data = await api.get('/users', { onFinally: () => setLoading(false) })// entities/user.ts
const api = new Request('/api')
export const UserApi = {
list: (page: number) =>
api.get<User[]>('/users', { page }),
get: (id: number) =>
api.get<User>(`/users/${id}`),
create: (data: CreateUserDto) =>
api.post<User>('/users', data, { onSuccess: () => toast.success('创建成功') }),
}
// 使用
const users = await UserApi.list(1)const cachedApi = new Request('/api')
// cache 和 dedup 是实例级别的,不同的 Request 实例互相隔离
const data1 = await cachedApi.get('/users', {}, { cache: { ttl: 5000 } })
const data2 = await cachedApi.get('/users', {}, { cache: { ttl: 5000 } }) // 命中缓存
const otherApi = new Request('/api') // 独立缓存interface RequestConfig {
// 请求参数
params?: Record<PropertyKey, any>
data?: unknown
// 拦截器(单次请求)
requestInterceptors?: RequestInterceptor[]
responseInterceptors?: ResponseInterceptor[]
errorInterceptors?: ErrorInterceptor[]
// 超时 & 重试
timeout?: number // 毫秒,超时自动 abort 当前尝试
retry?: number // 失败重试次数,0 = 不重试
retryDelay?: number // 重试间隔(毫秒)
// 缓存(仅 GET)
cache?: boolean | { ttl: number } // true = 默认 TTL 5s
// 下载进度
onDownloadProgress?: (loaded: number, total: number) => void
// 生命周期回调
onSuccess?: (data: unknown) => void
onError?: (error: unknown) => void
onAbort?: () => void
onFinally?: () => void
}支持 SSE 流式请求,基于 Fetch API 实现,相比原生 EventSource 有以下优势:
- ✅ 支持自定义 Headers
- ✅ 支持 POST 请求
- ✅ 支持所有 HTTP 方法
import { Request } from '@cc-heart/utils'
const api = new Request('https://api.example.com')
// GET SSE
const handle = api.sse('/events', {
onMessage(event) {
console.log('收到消息:', event.data)
},
onOpen() {
console.log('连接已建立')
},
onError(error) {
console.error('连接错误:', error)
},
onClose() {
console.log('连接已关闭')
}
})
// 取消连接
handle.abort()const handle = api.sse('/chat/completions', {
method: 'POST',
data: {
prompt: '你好',
model: 'gpt-4'
},
onMessage(event) {
// 解析 JSON 数据
try {
const data = JSON.parse(event.data)
console.log('AI 回复:', data.content)
} catch {
console.log('原始数据:', event.data)
}
},
onError(err) {
console.error('请求失败:', err)
}
})import type { RequestInterceptor } from '@cc-heart/utils'
const addAuth: RequestInterceptor = (config) => ({
...config,
headers: {
...config.headers,
Authorization: `Bearer ${getToken()}`
}
})
const api = new Request('https://api.example.com')
api.useRequestInterceptor(addAuth)
// SSE 请求会自动携带拦截器添加的 Headers
const handle = api.sse('/protected/events', {
onMessage(event) {
console.log(event.data)
}
})interface SSEMessageEvent {
event?: string // 事件类型
data: string // 消息数据
id?: string // 最后事件 ID
retry?: number // 重连间隔(毫秒)
}
interface SSECallbacks {
onMessage?: (event: SSEMessageEvent) => void
onOpen?: () => void
onError?: (error: unknown) => void
onClose?: () => void
}interface RequestHandle<T> {
// thenable,可直接 await
then, catch, finally: Promise 方法
// 取消请求
abort: () => void
}@cc-heart/utils 基于 MIT License 协议开源。
