📚 现代前端开发 目录
阶段5:全栈与部署
第 29 / 35 课

API设计与Fetch

前后端通信的桥梁

📖 核心概念

💻 代码实现

API客户端封装 ✅ typescript
// 类型安全的API客户端
class ApiClient {
  private baseUrl: string;
  private token: string | null = null;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  setToken(token: string) { this.token = token; }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = \`\${this.baseUrl}\${endpoint}\`;
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      ...options.headers as Record<string, string>,
    };

    if (this.token) {
      headers['Authorization'] = \`Bearer \${this.token}\`;
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 10000);

    try {
      const response = await fetch(url, {
        ...options,
        headers,
        signal: controller.signal,
      });

      clearTimeout(timeout);

      if (!response.ok) {
        const error = await response.json().catch(() => ({ message: response.statusText }));
        throw new ApiError(response.status, error.message || response.statusText, error);
      }

      if (response.status === 204) return null as T;
      return response.json();
    } catch (error) {
      clearTimeout(timeout);
      if (error instanceof DOMException && error.name === 'AbortError') {
        throw new ApiError(408, '请求超时', {});
      }
      throw error;
    }
  }

  get<T>(endpoint: string) { return this.request<T>(endpoint); }
  post<T>(endpoint: string, data: unknown) {
    return this.request<T>(endpoint, { method: 'POST', body: JSON.stringify(data) });
  }
  put<T>(endpoint: string, data: unknown) {
    return this.request<T>(endpoint, { method: 'PUT', body: JSON.stringify(data) });
  }
  patch<T>(endpoint: string, data: unknown) {
    return this.request<T>(endpoint, { method: 'PATCH', body: JSON.stringify(data) });
  }
  delete<T>(endpoint: string) { return this.request<T>(endpoint, { method: 'DELETE' }); }
}

class ApiError extends Error {
  constructor(public status: number, message: string, public data: unknown) {
    super(message);
    this.name = 'ApiError';
  }
}

// API类型定义
interface User { id: string; name: string; email: string; }
interface Post { id: string; title: string; content: string; author: User; }
interface PaginatedResponse<T> { data: T[]; total: number; page: number; pageSize: number; }

// 使用
const api = new ApiClient('/api');
api.setToken(localStorage.getItem('token') || '');

const users = await api.get<PaginatedResponse<User>>('/users?page=1&pageSize=10');
const post = await api.post<Post>('/posts', { title: 'Hello', content: 'World' });
React Query数据管理 ✅ tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 数据获取Hook
function useUsers(page: number) {
  return useQuery({
    queryKey: ['users', page],
    queryFn: () => api.get<PaginatedResponse<User>>(\`/users?page=\${page}\`),
    staleTime: 5 * 60 * 1000, // 5分钟内不重新请求
    placeholderData: (prev) => prev, // 切换页面时保留旧数据
  });
}

// 创建mutation
function useCreatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreatePostInput) => api.post<Post>('/posts', data),
    onSuccess: (newPost) => {
      // 乐观更新:直接修改缓存
      queryClient.setQueryData(['posts'], (old: Post[] | undefined) =>
        old ? [newPost, ...old] : [newPost]
      );
      // 或者让相关查询失效
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
}

// 乐观更新
function useUpdatePost() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: UpdatePostInput) => api.patch<Post>(\`/posts/\${data.id}\`, data),
    onMutate: async (newData) => {
      await queryClient.cancelQueries({ queryKey: ['posts', newData.id] });
      const previousPost = queryClient.getQueryData(['posts', newData.id]);
      queryClient.setQueryData(['posts', newData.id], (old: Post) => ({
        ...old, ...newData,
      }));
      return { previousPost };
    },
    onError: (err, newData, context) => {
      queryClient.setQueryData(['posts', newData.id], context?.previousPost);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
}

// 组件中使用
function UserList() {
  const [page, setPage] = useState(1);
  const { data, isLoading, error } = useUsers(page);

  if (isLoading) return <Skeleton />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      {data.data.map(user => <UserCard key={user.id} user={user} />)}
      <Pagination current={page} total={Math.ceil(data.total / data.pageSize)} onChange={setPage} />
    </div>
  );
}

🎯 练习任务

🏆 成就解锁
API架构师 — 掌握前后端通信设计,构建高效的API交互层