01MVP 标识01MVP
包文档基础设施服务api-client API 客户端

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 生成