api-client API 客户端
类型安全的 API 客户端工具包,提供请求处理、错误管理、缓存策略和性能监控
@mono/api-client
类型安全的 API 客户端工具包,基于 TanStack Query (React Query) 和 Hono RPC 客户端构建。提供全面的请求处理、错误管理、缓存策略和性能监控功能。
核心特性
- 类型安全的 API 调用:完整的 TypeScript 支持和 Hono RPC 客户端
- 智能缓存:多种缓存策略(实时、中等、稳定、静态)
- 错误处理:全面的错误类型和用户友好的错误消息
- 请求去重:自动去重并发请求
- 性能监控:内置性能跟踪和重复请求检测
- React Query 集成:预配置的常用 API 操作 hooks
- 重试逻辑:智能重试和指数退避
- 缓存失效:智能缓存失效策略
安装
pnpm add @mono/api-client核心组件
1. API 客户端 (Hono RPC)
使用 Hono 的 RPC 客户端实现类型安全的 API 调用:
import { apiClient, createApiClient } from '@mono/api-client';
// 使用默认客户端
const response = await apiClient.users.$get();
// 创建自定义客户端
const customClient = createApiClient('https://api.example.com');2. 通用 API 客户端
支持重试和超时的通用 HTTP 客户端:
import { ApiClient } from '@mono/api-client';
// GET 请求
const data = await ApiClient.get<User>('/api/users/123');
// POST 请求
const newUser = await ApiClient.post<User>('/api/users', {
name: '张三',
email: 'zhangsan@example.com'
});
// 自定义选项
const data = await ApiClient.get<User>('/api/users/123', {
timeout: 5000, // 5秒超时
retry: 2, // 重试2次
retryDelay: 500 // 重试延迟500ms
});3. Query 客户端
预配置的 TanStack Query 客户端:
import { createQueryClient } from '@mono/api-client';
const queryClient = createQueryClient();缓存策略
包提供四种缓存策略:
import { cacheConfig } from '@mono/api-client';
// 实时数据(5秒过期,1分钟垃圾回收)
cacheConfig.realtime
// 中等变化频率(2分钟过期,10分钟垃圾回收)
cacheConfig.moderate
// 稳定数据(5分钟过期,30分钟垃圾回收)
cacheConfig.stable
// 静态数据(15分钟过期,1小时垃圾回收)
cacheConfig.static在查询中使用
import { useQuery } from '@tanstack/react-query';
import { cacheConfig, queryKeys } from '@mono/api-client';
function useUserProfile() {
return useQuery({
queryKey: queryKeys.profile(),
queryFn: fetchProfile,
...cacheConfig.stable // 用户资料变化不频繁
});
}Query Keys
集中式查询键管理:
import { queryKeys } from '@mono/api-client';
// 用户资料
queryKeys.profile() // ['profile']
// 项目
queryKeys.projects() // ['projects']
queryKeys.projects({ userId: '123' }) // ['projects', { userId: '123' }]
// 活动
queryKeys.events.list() // ['events', 'list']
queryKeys.events.list({ type: 'workshop' }) // ['events', 'list', { type: 'workshop' }]
queryKeys.events.series.detail('series-id') // ['events', 'series', 'detail', 'series-id']
// 通知
queryKeys.notifications.list(1, 20) // ['notifications', 'list', 1, 20]
queryKeys.notifications.unreadCount() // ['notifications', 'unread-count']错误处理
错误类型
import { ErrorType, AppErrorHandler } from '@mono/api-client';
enum ErrorType {
NETWORK = 'NETWORK', // 网络错误
AUTHENTICATION = 'AUTHENTICATION', // 认证失败
PERMISSION = 'PERMISSION', // 权限不足
VALIDATION = 'VALIDATION', // 验证错误
NOT_FOUND = 'NOT_FOUND', // 资源不存在
SERVER = 'SERVER', // 服务器错误
RATE_LIMIT = 'RATE_LIMIT', // 请求频率限制
UNKNOWN = 'UNKNOWN' // 未知错误
}创建错误
import { AppErrorHandler, ErrorType } from '@mono/api-client';
// 创建自定义错误
const error = AppErrorHandler.createError(
ErrorType.VALIDATION,
'Invalid email format',
'邮箱格式不正确'
);
// 从 fetch 响应创建错误
const error = AppErrorHandler.fromFetchResponse(response);
// 处理错误并显示 toast
AppErrorHandler.handleError(error, true);API 调用中的错误处理
try {
const data = await ApiClient.get('/api/users');
} catch (error) {
const appError = AppErrorHandler.handleError(error as Error);
// 错误已记录并显示 toast
}React Query Hooks
用户资料和项目
import {
useProfileQuery,
useProjectsQuery,
useParticipatedProjectsQuery
} from '@mono/api-client';
function Dashboard() {
const { data: profile } = useProfileQuery();
const { data: projects } = useProjectsQuery();
const { data: participated } = useParticipatedProjectsQuery();
return (
<div>
<h1>欢迎,{profile?.name}</h1>
<ProjectList projects={projects} />
</div>
);
}通知
import {
useNotificationsQuery,
useUnreadNotificationsCountQuery,
useMarkNotificationAsReadMutation,
useMarkAllNotificationsAsReadMutation
} from '@mono/api-client';
function Notifications() {
const { data: notifications } = useNotificationsQuery(1, 20);
const { data: unreadCount } = useUnreadNotificationsCountQuery();
const markAsRead = useMarkNotificationAsReadMutation();
const markAllAsRead = useMarkAllNotificationsAsReadMutation();
const handleMarkAsRead = (id: string) => {
markAsRead.mutate(id);
};
return (
<div>
<h2>通知 ({unreadCount})</h2>
<button onClick={() => markAllAsRead.mutate()}>
全部标记为已读
</button>
{notifications?.map(notification => (
<NotificationItem
key={notification.id}
notification={notification}
onMarkAsRead={handleMarkAsRead}
/>
))}
</div>
);
}活动
import {
useEventsListQuery,
useEventSeriesListQuery,
useEventSeriesDetailQuery,
useCreateEventSeriesMutation
} from '@mono/api-client';
function Events() {
const { data: events } = useEventsListQuery({
type: 'workshop',
status: 'upcoming'
});
const { data: seriesData } = useEventSeriesListQuery({
page: 1,
limit: 10
});
const createSeries = useCreateEventSeriesMutation();
const handleCreateSeries = async (data: EventSeriesPayload) => {
await createSeries.mutateAsync(data);
};
return (
<div>
<EventList events={events} />
<EventSeriesList series={seriesData?.series} />
</div>
);
}缓存失效
智能缓存失效策略:
import { cacheInvalidation } from '@mono/api-client';
import { useQueryClient } from '@tanstack/react-query';
function MyComponent() {
const queryClient = useQueryClient();
// 用户资料更新后
cacheInvalidation.onProfileUpdate(queryClient);
// 项目更新后
cacheInvalidation.onProjectUpdate(queryClient, userId);
// 通知更新后
cacheInvalidation.onNotificationUpdate(queryClient);
// 认证状态变化后(登录/登出)
cacheInvalidation.onAuthChange(queryClient, isLoggedIn);
}请求去重
自动去重并发请求:
import { requestDeduplicator } from '@mono/api-client';
// 多个并发调用只会触发一次请求
const data1 = requestDeduplicator.deduplicate('profile', fetchProfile);
const data2 = requestDeduplicator.deduplicate('profile', fetchProfile);
// 只会执行一次 fetchProfile()
// 清除特定键
requestDeduplicator.clear('profile');
// 清除所有
requestDeduplicator.clear();性能监控
内置开发环境性能监控:
import {
ApiPerformanceMonitor,
setupQueryClientMonitoring,
generatePerformanceReport
} from '@mono/api-client';
// 设置监控
const queryClient = createQueryClient();
setupQueryClientMonitoring(queryClient);
// 获取统计信息
const monitor = ApiPerformanceMonitor.getInstance();
const stats = monitor.getStats();
// 生成报告(仅开发模式)
generatePerformanceReport();性能特性
- 请求日志:跟踪所有 API 请求及其持续时间
- 重复检测:警告 5 秒内的重复请求
- 自动报告:开发模式下每分钟生成一次报告
- Query 监控:跟踪 TanStack Query 性能
API Fetchers
服务端和客户端数据获取工具:
import {
fetchEventsList,
fetchEventSeriesList,
fetchEventSeriesDetail,
fetchEventsOrganizations
} from '@mono/api-client';
// 获取活动列表(带缓存)
const events = await fetchEventsList(
{ type: 'workshop', status: 'upcoming' },
{ next: { revalidate: 30 } } // 30秒缓存
);
// 获取活动系列
const { series, pagination } = await fetchEventSeriesList({
page: 1,
limit: 20,
search: '技术'
});
// 获取活动系列详情
const seriesDetail = await fetchEventSeriesDetail('series-slug');键盘检测 Hook
检测移动端虚拟键盘可见性:
import { useKeyboardDetection } from '@mono/api-client';
function ChatInput() {
const isKeyboardVisible = useKeyboardDetection();
return (
<div className={isKeyboardVisible ? 'keyboard-open' : ''}>
<input type="text" placeholder="输入消息..." />
</div>
);
}配置选项
API 请求选项
interface ApiRequestOptions extends RequestInit {
timeout?: number; // 默认: 10000ms
retry?: number; // 默认: 3
retryDelay?: number; // 默认: 1000ms
}Query 客户端默认配置
{
staleTime: 2 * 60 * 1000, // 2 分钟
gcTime: 10 * 60 * 1000, // 10 分钟
retry: 2, // 最多重试 2 次
retryDelay: exponential, // 指数退避
refetchOnWindowFocus: true, // 窗口聚焦时重新获取
refetchOnReconnect: true, // 重新连接时重新获取
refetchOnMount: true // 挂载时重新获取
}最佳实践
1. 使用合适的缓存策略
// 实时数据(通知、实时更新)
useQuery({ ...cacheConfig.realtime })
// 中等频率数据(项目、用户列表)
useQuery({ ...cacheConfig.moderate })
// 稳定数据(用户资料、设置)
useQuery({ ...cacheConfig.stable })
// 静态数据(配置、常量)
useQuery({ ...cacheConfig.static })2. 优雅处理错误
const { data, error, isError } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: (failureCount, error) => {
// 认证错误不重试
if (error.message.includes('401')) return false;
return failureCount < 2;
}
});
if (isError) {
AppErrorHandler.handleError(error as Error);
}3. Mutation 后失效缓存
const createProject = useMutation({
mutationFn: createProjectAPI,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.projects()
});
}
});4. 一致使用 Query Keys
// 好的做法:使用集中式 query keys
queryKeys.events.list({ type: 'workshop' })
// 不好的做法:硬编码 query keys
['events', 'list', { type: 'workshop' }]类型安全
所有 API 调用都是完全类型化的:
// 响应类型自动推断
const user = await ApiClient.get<User>('/api/users/123');
// user 的类型是 User
// 请求负载类型化
const newUser = await ApiClient.post<User, CreateUserPayload>(
'/api/users',
{ name: '张三', email: 'zhangsan@example.com' }
);实际应用示例
完整的数据获取流程
import {
useEventsListQuery,
useEventSeriesSubscriptionQuery,
useSubscribeEventSeriesMutation,
cacheConfig,
AppErrorHandler
} from '@mono/api-client';
function EventSeriesPage({ seriesId }: { seriesId: string }) {
// 获取活动列表
const {
data: events,
isLoading,
error
} = useEventsListQuery(
{ seriesId },
{ enabled: !!seriesId }
);
// 获取订阅状态
const { data: subscription } = useEventSeriesSubscriptionQuery(
seriesId,
{ enabled: !!seriesId }
);
// 订阅 mutation
const subscribe = useSubscribeEventSeriesMutation();
const handleSubscribe = async () => {
try {
await subscribe.mutateAsync({
identifier: seriesId,
payload: {
notifyEmail: true,
notifyInApp: true
}
});
} catch (error) {
AppErrorHandler.handleError(error as Error);
}
};
if (isLoading) return <Loading />;
if (error) return <Error error={error} />;
return (
<div>
<h1>活动系列</h1>
<button
onClick={handleSubscribe}
disabled={subscription?.subscribed}
>
{subscription?.subscribed ? '已订阅' : '订阅'}
</button>
<EventList events={events} />
</div>
);
}自定义 Hook 封装
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { cacheConfig, queryKeys, AppErrorHandler } from '@mono/api-client';
// 自定义 hook
export function useUserBookmarks() {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: queryKeys.bookmarks.projects(),
queryFn: fetchUserBookmarks,
...cacheConfig.moderate
});
const addBookmark = useMutation({
mutationFn: addBookmarkAPI,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.bookmarks.projects()
});
},
onError: (error) => {
AppErrorHandler.handleError(error as Error);
}
});
const removeBookmark = useMutation({
mutationFn: removeBookmarkAPI,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.bookmarks.projects()
});
},
onError: (error) => {
AppErrorHandler.handleError(error as Error);
}
});
return {
bookmarks: query.data,
isLoading: query.isLoading,
addBookmark: addBookmark.mutate,
removeBookmark: removeBookmark.mutate
};
}相关包
@mono/utils: 工具函数(getBaseUrl)@tanstack/react-query: React Query 库hono: Hono Web 框架sonner: Toast 通知zod: Schema 验证
测试
查看 src/__tests__/ 中的测试文件以获取全面的示例:
api-client.test.ts: API 客户端功能error-handler.test.ts: 错误处理cache-config.test.ts: 缓存策略query-keys.test.ts: Query key 生成