Skip to content

数据获取最佳实践

概述

数据获取是React应用的核心功能之一,采用正确的实践可以显著提升应用性能和用户体验。本文将总结数据获取的最佳实践,涵盖错误处理、性能优化、安全性、可维护性等方面。

架构设计

API层抽象

tsx
// api/client.ts
import axios from 'axios';

class APIClient {
  private client = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    timeout: 10000,
  });
  
  constructor() {
    this.setupInterceptors();
  }
  
  private setupInterceptors() {
    // 请求拦截
    this.client.interceptors.request.use(
      (config) => {
        const token = this.getToken();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
    
    // 响应拦截
    this.client.interceptors.response.use(
      (response) => response.data,
      (error) => this.handleError(error)
    );
  }
  
  private getToken() {
    return localStorage.getItem('token');
  }
  
  private handleError(error: any) {
    if (error.response?.status === 401) {
      this.logout();
    }
    return Promise.reject(error);
  }
  
  private logout() {
    localStorage.removeItem('token');
    window.location.href = '/login';
  }
  
  // HTTP方法
  async get<T>(url: string, config?: any): Promise<T> {
    return this.client.get(url, config);
  }
  
  async post<T>(url: string, data?: any, config?: any): Promise<T> {
    return this.client.post(url, data, config);
  }
  
  async put<T>(url: string, data?: any, config?: any): Promise<T> {
    return this.client.put(url, data, config);
  }
  
  async delete<T>(url: string, config?: any): Promise<T> {
    return this.client.delete(url, config);
  }
}

export const apiClient = new APIClient();

服务层封装

tsx
// services/userService.ts
import { apiClient } from '../api/client';

export interface User {
  id: string;
  name: string;
  email: string;
}

export interface CreateUserInput {
  name: string;
  email: string;
}

export const userService = {
  getUsers: () => 
    apiClient.get<User[]>('/users'),
  
  getUser: (id: string) => 
    apiClient.get<User>(`/users/${id}`),
  
  createUser: (data: CreateUserInput) => 
    apiClient.post<User>('/users', data),
  
  updateUser: (id: string, data: Partial<User>) => 
    apiClient.put<User>(`/users/${id}`, data),
  
  deleteUser: (id: string) => 
    apiClient.delete<void>(`/users/${id}`),
};

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '../services/userService';

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: userService.getUsers,
  });
}

export function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => userService.getUser(id),
    enabled: !!id,
  });
}

export function useCreateUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: userService.createUser,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

错误处理

统一错误处理

tsx
// types/errors.ts
export class APIError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public code?: string,
    public data?: any
  ) {
    super(message);
    this.name = 'APIError';
  }
}

// utils/errorHandler.ts
import { toast } from 'react-hot-toast';

export function handleAPIError(error: unknown) {
  if (error instanceof APIError) {
    switch (error.statusCode) {
      case 400:
        toast.error(`Invalid request: ${error.message}`);
        break;
      case 401:
        toast.error('Please login again');
        window.location.href = '/login';
        break;
      case 403:
        toast.error('You do not have permission');
        break;
      case 404:
        toast.error('Resource not found');
        break;
      case 500:
        toast.error('Server error, please try again later');
        break;
      default:
        toast.error(error.message || 'An error occurred');
    }
  } else if (error instanceof Error) {
    toast.error(error.message);
  } else {
    toast.error('Unknown error occurred');
  }
}

// 使用
function UserList() {
  const { data, error } = useUsers();
  
  useEffect(() => {
    if (error) {
      handleAPIError(error);
    }
  }, [error]);
  
  return <div>...</div>;
}

错误边界集成

tsx
// components/ErrorBoundary.tsx
import { Component, ReactNode } from 'react';
import { APIError } from '../types/errors';

interface Props {
  children: ReactNode;
  fallback?: (error: Error) => ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error: Error, errorInfo: any) {
    // 发送到错误追踪服务
    console.error('Error caught by boundary:', error, errorInfo);
    
    // Sentry, LogRocket等
    if (process.env.NODE_ENV === 'production') {
      // sentryLog(error, errorInfo);
    }
  }
  
  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback(this.state.error!);
      }
      
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error?.message}</pre>
          </details>
          <button onClick={() => window.location.reload()}>
            Reload page
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

性能优化

请求去重

tsx
// utils/dedupeRequests.ts
const pendingRequests = new Map<string, Promise<any>>();

export function dedupeRequest<T>(
  key: string,
  fetcher: () => Promise<T>
): Promise<T> {
  // 如果有正在进行的相同请求,返回该Promise
  if (pendingRequests.has(key)) {
    return pendingRequests.get(key)!;
  }
  
  // 创建新请求
  const promise = fetcher().finally(() => {
    // 请求完成后移除
    pendingRequests.delete(key);
  });
  
  pendingRequests.set(key, promise);
  
  return promise;
}

// 使用
async function fetchUser(id: string) {
  return dedupeRequest(`user-${id}`, () => 
    apiClient.get(`/users/${id}`)
  );
}

数据预加载

tsx
// hooks/usePrefetchUser.ts
import { useQueryClient } from '@tanstack/react-query';
import { userService } from '../services/userService';

export function usePrefetchUser() {
  const queryClient = useQueryClient();
  
  return (userId: string) => {
    queryClient.prefetchQuery({
      queryKey: ['user', userId],
      queryFn: () => userService.getUser(userId),
      staleTime: 5 * 60 * 1000, // 5分钟
    });
  };
}

// 使用
function UserList() {
  const { data: users } = useUsers();
  const prefetchUser = usePrefetchUser();
  
  return (
    <ul>
      {users?.map(user => (
        <li
          key={user.id}
          onMouseEnter={() => prefetchUser(user.id)}
        >
          <Link to={`/users/${user.id}`}>{user.name}</Link>
        </li>
      ))}
    </ul>
  );
}

虚拟滚动

tsx
import { useVirtualizer } from '@tanstack/react-virtual';
import { useInfiniteQuery } from '@tanstack/react-query';

function VirtualizedList() {
  const parentRef = useRef<HTMLDivElement>(null);
  
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['items'],
    queryFn: ({ pageParam = 0 }) => fetchItems(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor,
  });
  
  const allItems = data?.pages.flatMap(page => page.items) ?? [];
  
  const virtualizer = useVirtualizer({
    count: hasNextPage ? allItems.length + 1 : allItems.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });
  
  useEffect(() => {
    const [lastItem] = [...virtualizer.getVirtualItems()].reverse();
    
    if (!lastItem) return;
    
    if (
      lastItem.index >= allItems.length - 1 &&
      hasNextPage &&
      !isFetchingNextPage
    ) {
      fetchNextPage();
    }
  }, [
    hasNextPage,
    fetchNextPage,
    allItems.length,
    isFetchingNextPage,
    virtualizer.getVirtualItems(),
  ]);
  
  return (
    <div ref={parentRef} style={{ height: '500px', overflow: 'auto' }}>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map(virtualRow => {
          const item = allItems[virtualRow.index];
          
          return (
            <div
              key={virtualRow.index}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualRow.size}px`,
                transform: `translateY(${virtualRow.start}px)`,
              }}
            >
              {item ? (
                <ItemCard item={item} />
              ) : (
                <div>Loading...</div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

缓存策略

智能缓存时间

tsx
// config/cacheConfig.ts
export const CACHE_TIME = {
  // 静态数据 - 长时间缓存
  STATIC: {
    staleTime: Infinity,
    cacheTime: 24 * 60 * 60 * 1000, // 24小时
  },
  
  // 用户数据 - 中等缓存
  USER: {
    staleTime: 5 * 60 * 1000,       // 5分钟
    cacheTime: 10 * 60 * 1000,      // 10分钟
  },
  
  // 实时数据 - 短时间缓存
  REALTIME: {
    staleTime: 0,
    cacheTime: 1000,                // 1秒
    refetchInterval: 3000,          // 3秒轮询
  },
  
  // 列表数据
  LIST: {
    staleTime: 30 * 1000,           // 30秒
    cacheTime: 5 * 60 * 1000,       // 5分钟
  },
};

// 使用
function useConfig() {
  return useQuery({
    queryKey: ['config'],
    queryFn: fetchConfig,
    ...CACHE_TIME.STATIC,
  });
}

function useUserProfile(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    ...CACHE_TIME.USER,
  });
}

缓存失效策略

tsx
// utils/cacheInvalidation.ts
import { QueryClient } from '@tanstack/react-query';

export function invalidateUserRelatedQueries(
  queryClient: QueryClient,
  userId: string
) {
  // 失效用户相关的所有查询
  return Promise.all([
    queryClient.invalidateQueries({ queryKey: ['user', userId] }),
    queryClient.invalidateQueries({ queryKey: ['user', userId, 'posts'] }),
    queryClient.invalidateQueries({ queryKey: ['user', userId, 'comments'] }),
  ]);
}

export function invalidateTeamQueries(
  queryClient: QueryClient,
  teamId: string
) {
  return Promise.all([
    queryClient.invalidateQueries({ queryKey: ['team', teamId] }),
    queryClient.invalidateQueries({ queryKey: ['team', teamId, 'members'] }),
    queryClient.invalidateQueries({ queryKey: ['team', teamId, 'projects'] }),
  ]);
}

// 使用
function useUpdateUser() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: updateUserAPI,
    onSuccess: (data) => {
      invalidateUserRelatedQueries(queryClient, data.id);
      
      // 如果改变了团队
      if (data.teamId) {
        invalidateTeamQueries(queryClient, data.teamId);
      }
    },
  });
}

类型安全

严格的类型定义

tsx
// types/api.ts
export interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}

export interface APIResponse<T> {
  success: boolean;
  data: T;
  error?: {
    code: string;
    message: string;
  };
}

// 泛型Hook
export function usePaginatedQuery<T>(
  key: string,
  fetcher: (page: number) => Promise<PaginatedResponse<T>>,
  initialPage = 0
) {
  const [page, setPage] = useState(initialPage);
  
  const query = useQuery({
    queryKey: [key, page],
    queryFn: () => fetcher(page),
  });
  
  return {
    ...query,
    page,
    setPage,
    nextPage: () => setPage(p => p + 1),
    prevPage: () => setPage(p => Math.max(0, p - 1)),
  };
}

// 使用
function UserList() {
  const {
    data,
    page,
    nextPage,
    prevPage,
    isLoading,
  } = usePaginatedQuery<User>(
    'users',
    (page) => fetchUsers({ page, pageSize: 10 })
  );
  
  return (
    <div>
      {data?.data.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
      
      <Pagination
        page={page}
        hasMore={data?.hasMore}
        onNext={nextPage}
        onPrev={prevPage}
      />
    </div>
  );
}

安全性

请求认证

tsx
// utils/auth.ts
export class AuthManager {
  private static TOKEN_KEY = 'auth_token';
  private static REFRESH_TOKEN_KEY = 'refresh_token';
  
  static getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }
  
  static setToken(token: string): void {
    localStorage.setItem(this.TOKEN_KEY, token);
  }
  
  static getRefreshToken(): string | null {
    return localStorage.getItem(this.REFRESH_TOKEN_KEY);
  }
  
  static setRefreshToken(token: string): void {
    localStorage.setItem(this.REFRESH_TOKEN_KEY, token);
  }
  
  static clearTokens(): void {
    localStorage.removeItem(this.TOKEN_KEY);
    localStorage.removeItem(this.REFRESH_TOKEN_KEY);
  }
  
  static async refreshToken(): Promise<string> {
    const refreshToken = this.getRefreshToken();
    
    if (!refreshToken) {
      throw new Error('No refresh token');
    }
    
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken }),
    });
    
    if (!response.ok) {
      this.clearTokens();
      window.location.href = '/login';
      throw new Error('Refresh failed');
    }
    
    const { token } = await response.json();
    this.setToken(token);
    
    return token;
  }
}

// API Client集成
this.client.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        const token = await AuthManager.refreshToken();
        originalRequest.headers.Authorization = `Bearer ${token}`;
        return this.client(originalRequest);
      } catch (refreshError) {
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

数据验证

tsx
import { z } from 'zod';

// 定义Schema
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
});

// 验证响应数据
async function fetchUser(id: string) {
  const response = await apiClient.get(`/users/${id}`);
  
  try {
    const user = UserSchema.parse(response);
    return user;
  } catch (error) {
    console.error('Invalid user data:', error);
    throw new Error('Invalid server response');
  }
}

// 验证请求数据
const CreateUserSchema = UserSchema.omit({ id: true });

async function createUser(data: unknown) {
  const validated = CreateUserSchema.parse(data);
  return apiClient.post('/users', validated);
}

监控和调试

请求日志

tsx
// utils/requestLogger.ts
interface RequestLog {
  id: string;
  method: string;
  url: string;
  status?: number;
  duration?: number;
  error?: string;
  timestamp: number;
}

class RequestLogger {
  private logs: RequestLog[] = [];
  private maxLogs = 100;
  
  log(log: RequestLog) {
    this.logs.push(log);
    
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }
    
    if (process.env.NODE_ENV === 'development') {
      console.log('[API]', log);
    }
  }
  
  getLogs() {
    return this.logs;
  }
  
  clearLogs() {
    this.logs = [];
  }
}

export const requestLogger = new RequestLogger();

// 集成到拦截器
this.client.interceptors.request.use((config) => {
  config.metadata = { startTime: Date.now(), id: generateId() };
  return config;
});

this.client.interceptors.response.use(
  (response) => {
    const duration = Date.now() - response.config.metadata.startTime;
    
    requestLogger.log({
      id: response.config.metadata.id,
      method: response.config.method!,
      url: response.config.url!,
      status: response.status,
      duration,
      timestamp: Date.now(),
    });
    
    return response;
  },
  (error) => {
    const duration = Date.now() - error.config.metadata.startTime;
    
    requestLogger.log({
      id: error.config.metadata.id,
      method: error.config.method,
      url: error.config.url,
      status: error.response?.status,
      duration,
      error: error.message,
      timestamp: Date.now(),
    });
    
    return Promise.reject(error);
  }
);

性能监控

tsx
// utils/performance.ts
export function measurePerformance<T>(
  name: string,
  fn: () => Promise<T>
): Promise<T> {
  const startTime = performance.now();
  
  return fn().then(
    (result) => {
      const duration = performance.now() - startTime;
      
      // 发送到分析服务
      if (window.gtag) {
        window.gtag('event', 'api_call', {
          event_category: 'API',
          event_label: name,
          value: Math.round(duration),
        });
      }
      
      console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`);
      
      return result;
    },
    (error) => {
      const duration = performance.now() - startTime;
      console.error(`[Performance] ${name} failed: ${duration.toFixed(2)}ms`);
      throw error;
    }
  );
}

// 使用
async function fetchUsers() {
  return measurePerformance('fetchUsers', () =>
    apiClient.get('/users')
  );
}

总结

数据获取最佳实践核心要点:

  1. 架构设计:API层抽象、服务层封装
  2. 错误处理:统一处理、错误边界
  3. 性能优化:请求去重、预加载、虚拟滚动
  4. 缓存策略:智能缓存时间、失效策略
  5. 类型安全:严格类型定义、泛型封装
  6. 安全性:认证管理、数据验证
  7. 监控调试:请求日志、性能监控

遵循这些最佳实践可以构建高性能、可维护的React应用。

高级最佳实践

请求优先级管理

tsx
// utils/requestPriority.ts
export enum RequestPriority {
  CRITICAL = 'critical',    // 用户交互相关
  HIGH = 'high',           // 首屏可见内容
  NORMAL = 'normal',       // 次要内容
  LOW = 'low',             // 预加载内容
}

interface PriorityRequest {
  id: string;
  priority: RequestPriority;
  fetcher: () => Promise<any>;
  resolve: (value: any) => void;
  reject: (error: any) => void;
}

class RequestQueue {
  private queues: Map<RequestPriority, PriorityRequest[]> = new Map([
    [RequestPriority.CRITICAL, []],
    [RequestPriority.HIGH, []],
    [RequestPriority.NORMAL, []],
    [RequestPriority.LOW, []],
  ]);
  
  private processing = false;
  private maxConcurrent = 6; // Chrome限制
  private activeRequests = 0;
  
  enqueue<T>(
    fetcher: () => Promise<T>,
    priority: RequestPriority = RequestPriority.NORMAL
  ): Promise<T> {
    return new Promise((resolve, reject) => {
      const request: PriorityRequest = {
        id: Math.random().toString(36),
        priority,
        fetcher,
        resolve,
        reject,
      };
      
      this.queues.get(priority)!.push(request);
      this.process();
    });
  }
  
  private async process() {
    if (this.processing || this.activeRequests >= this.maxConcurrent) {
      return;
    }
    
    this.processing = true;
    
    // 按优先级处理
    const priorities = [
      RequestPriority.CRITICAL,
      RequestPriority.HIGH,
      RequestPriority.NORMAL,
      RequestPriority.LOW,
    ];
    
    for (const priority of priorities) {
      const queue = this.queues.get(priority)!;
      
      while (queue.length > 0 && this.activeRequests < this.maxConcurrent) {
        const request = queue.shift()!;
        this.activeRequests++;
        
        request.fetcher()
          .then(request.resolve)
          .catch(request.reject)
          .finally(() => {
            this.activeRequests--;
            this.processing = false;
            this.process();
          });
      }
    }
    
    this.processing = false;
  }
}

export const requestQueue = new RequestQueue();

// 使用
function useUser(id: string) {
  return useQuery({
    queryKey: ['user', id],
    queryFn: () => requestQueue.enqueue(
      () => apiClient.get(`/users/${id}`),
      RequestPriority.HIGH
    ),
  });
}

function usePrefetchUser(id: string) {
  const queryClient = useQueryClient();
  
  return () => {
    queryClient.prefetchQuery({
      queryKey: ['user', id],
      queryFn: () => requestQueue.enqueue(
        () => apiClient.get(`/users/${id}`),
        RequestPriority.LOW // 预加载使用低优先级
      ),
    });
  };
}

智能重试机制

tsx
// utils/smartRetry.ts
interface RetryConfig {
  maxRetries: number;
  baseDelay: number;
  maxDelay: number;
  shouldRetry: (error: any, attemptNumber: number) => boolean;
  onRetry?: (attemptNumber: number) => void;
}

export async function smartRetry<T>(
  fn: () => Promise<T>,
  config: RetryConfig
): Promise<T> {
  let lastError: any;
  
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      if (attempt === config.maxRetries || !config.shouldRetry(error, attempt)) {
        throw error;
      }
      
      // 计算延迟时间(指数退避 + 抖动)
      const exponentialDelay = Math.min(
        config.baseDelay * Math.pow(2, attempt),
        config.maxDelay
      );
      
      const jitter = exponentialDelay * 0.3 * Math.random();
      const delay = exponentialDelay + jitter;
      
      config.onRetry?.(attempt + 1);
      
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

// 网络错误识别
function isNetworkError(error: any): boolean {
  return (
    error.message === 'Network request failed' ||
    error.message === 'Failed to fetch' ||
    error.code === 'ECONNABORTED' ||
    error.code === 'ETIMEDOUT'
  );
}

// 可重试错误识别
function isRetryableError(error: any): boolean {
  if (isNetworkError(error)) return true;
  
  const status = error.response?.status;
  
  // 服务器错误可重试
  if (status >= 500 && status < 600) return true;
  
  // 429 Too Many Requests可重试
  if (status === 429) return true;
  
  // 408 Request Timeout可重试
  if (status === 408) return true;
  
  return false;
}

// 使用示例
async function fetchWithRetry<T>(url: string): Promise<T> {
  return smartRetry(
    () => apiClient.get<T>(url),
    {
      maxRetries: 3,
      baseDelay: 1000,
      maxDelay: 10000,
      shouldRetry: (error, attempt) => {
        if (!isRetryableError(error)) return false;
        
        // 429错误需要等待更长时间
        if (error.response?.status === 429) {
          const retryAfter = error.response.headers['retry-after'];
          if (retryAfter) {
            const delay = parseInt(retryAfter) * 1000;
            return delay < 30000; // 最多等待30秒
          }
        }
        
        return attempt < 3;
      },
      onRetry: (attempt) => {
        console.log(`Retrying request, attempt ${attempt}`);
      },
    }
  );
}

// React Query集成
function useSmartQuery<T>(key: string, url: string) {
  return useQuery({
    queryKey: [key],
    queryFn: () => fetchWithRetry<T>(url),
    retry: false, // 使用自定义重试
  });
}

数据预测与预加载

tsx
// utils/dataPrediction.ts
interface UserBehavior {
  timestamp: number;
  action: string;
  data: any;
}

class BehaviorPredictor {
  private history: UserBehavior[] = [];
  private maxHistory = 100;
  
  recordAction(action: string, data: any) {
    this.history.push({
      timestamp: Date.now(),
      action,
      data,
    });
    
    if (this.history.length > this.maxHistory) {
      this.history.shift();
    }
  }
  
  // 预测下一步操作
  predictNext(): string | null {
    if (this.history.length < 3) return null;
    
    const recent = this.history.slice(-5);
    const patterns: Map<string, number> = new Map();
    
    for (let i = 0; i < recent.length - 1; i++) {
      const current = recent[i].action;
      const next = recent[i + 1].action;
      const pattern = `${current}->${next}`;
      
      patterns.set(pattern, (patterns.get(pattern) || 0) + 1);
    }
    
    // 找出最频繁的模式
    let maxCount = 0;
    let prediction: string | null = null;
    
    patterns.forEach((count, pattern) => {
      if (count > maxCount) {
        maxCount = count;
        prediction = pattern.split('->')[1];
      }
    });
    
    return prediction;
  }
  
  // 预测用户会访问的页面
  predictRoutes(): string[] {
    const routeFrequency: Map<string, number> = new Map();
    
    this.history.forEach(({ action, data }) => {
      if (action === 'navigate') {
        const route = data.route;
        routeFrequency.set(route, (routeFrequency.get(route) || 0) + 1);
      }
    });
    
    // 返回访问频率最高的3个路由
    return Array.from(routeFrequency.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 3)
      .map(([route]) => route);
  }
}

export const behaviorPredictor = new BehaviorPredictor();

// 智能预加载Hook
function useSmartPrefetch() {
  const queryClient = useQueryClient();
  const location = useLocation();
  
  useEffect(() => {
    behaviorPredictor.recordAction('navigate', {
      route: location.pathname,
    });
    
    const predictedRoutes = behaviorPredictor.predictRoutes();
    
    // 预加载预测的路由数据
    predictedRoutes.forEach(route => {
      if (route.startsWith('/users/')) {
        const userId = route.split('/')[2];
        queryClient.prefetchQuery({
          queryKey: ['user', userId],
          queryFn: () => fetchUser(userId),
        });
      }
    });
  }, [location, queryClient]);
}

// 使用
function App() {
  useSmartPrefetch();
  
  return <Routes>...</Routes>;
}

离线同步策略

tsx
// utils/offlineSync.ts
interface SyncQueueItem {
  id: string;
  type: 'create' | 'update' | 'delete';
  resource: string;
  data: any;
  timestamp: number;
  retries: number;
}

class OfflineSyncManager {
  private queue: SyncQueueItem[] = [];
  private syncing = false;
  private db: IDBDatabase | null = null;
  
  async init() {
    return new Promise<void>((resolve, reject) => {
      const request = indexedDB.open('OfflineSync', 1);
      
      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        if (!db.objectStoreNames.contains('syncQueue')) {
          db.createObjectStore('syncQueue', { keyPath: 'id' });
        }
      };
      
      request.onsuccess = (event) => {
        this.db = (event.target as IDBOpenDBRequest).result;
        this.loadQueue().then(resolve);
      };
      
      request.onerror = () => reject(request.error);
    });
  }
  
  private async loadQueue() {
    if (!this.db) return;
    
    const transaction = this.db.transaction(['syncQueue'], 'readonly');
    const store = transaction.objectStore('syncQueue');
    const request = store.getAll();
    
    return new Promise<void>((resolve) => {
      request.onsuccess = () => {
        this.queue = request.result;
        resolve();
      };
    });
  }
  
  private async saveQueue() {
    if (!this.db) return;
    
    const transaction = this.db.transaction(['syncQueue'], 'readwrite');
    const store = transaction.objectStore('syncQueue');
    
    store.clear();
    this.queue.forEach(item => store.add(item));
  }
  
  enqueue(item: Omit<SyncQueueItem, 'id' | 'timestamp' | 'retries'>) {
    const queueItem: SyncQueueItem = {
      ...item,
      id: Math.random().toString(36),
      timestamp: Date.now(),
      retries: 0,
    };
    
    this.queue.push(queueItem);
    this.saveQueue();
    
    if (navigator.onLine) {
      this.sync();
    }
  }
  
  async sync() {
    if (this.syncing || this.queue.length === 0 || !navigator.onLine) {
      return;
    }
    
    this.syncing = true;
    
    const item = this.queue[0];
    
    try {
      await this.syncItem(item);
      this.queue.shift();
      await this.saveQueue();
      
      // 继续同步下一个
      this.syncing = false;
      this.sync();
    } catch (error) {
      item.retries++;
      
      if (item.retries >= 3) {
        // 重试次数过多,移除
        this.queue.shift();
        console.error('Sync failed after 3 retries:', item);
      }
      
      await this.saveQueue();
      this.syncing = false;
    }
  }
  
  private async syncItem(item: SyncQueueItem) {
    const { type, resource, data } = item;
    
    switch (type) {
      case 'create':
        await apiClient.post(`/${resource}`, data);
        break;
      case 'update':
        await apiClient.put(`/${resource}/${data.id}`, data);
        break;
      case 'delete':
        await apiClient.delete(`/${resource}/${data.id}`);
        break;
    }
  }
  
  getQueueLength() {
    return this.queue.length;
  }
}

export const offlineSyncManager = new OfflineSyncManager();

// 初始化
offlineSyncManager.init();

// 监听网络状态
window.addEventListener('online', () => {
  offlineSyncManager.sync();
});

// React Query集成
function useOfflineMutation() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (data: any) => {
      if (!navigator.onLine) {
        // 离线时加入队列
        offlineSyncManager.enqueue({
          type: 'create',
          resource: 'users',
          data,
        });
        
        // 乐观更新
        return data;
      }
      
      return apiClient.post('/users', data);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

// 显示同步状态
function SyncStatus() {
  const [queueLength, setQueueLength] = useState(0);
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const updateStatus = () => {
      setQueueLength(offlineSyncManager.getQueueLength());
      setIsOnline(navigator.onLine);
    };
    
    const interval = setInterval(updateStatus, 1000);
    
    window.addEventListener('online', updateStatus);
    window.addEventListener('offline', updateStatus);
    
    return () => {
      clearInterval(interval);
      window.removeEventListener('online', updateStatus);
      window.removeEventListener('offline', updateStatus);
    };
  }, []);
  
  if (queueLength === 0) return null;
  
  return (
    <div className="sync-status">
      {isOnline ? (
        <span>Syncing {queueLength} items...</span>
      ) : (
        <span>{queueLength} items pending sync</span>
      )}
    </div>
  );
}

请求合并与批处理

tsx
// utils/requestBatching.ts
interface BatchRequest<T = any> {
  id: string;
  params: any;
  resolve: (value: T) => void;
  reject: (error: any) => void;
}

class RequestBatcher<T = any> {
  private queue: BatchRequest<T>[] = [];
  private timer: NodeJS.Timeout | null = null;
  private batchDelay = 50; // ms
  
  constructor(
    private batchFetcher: (params: any[]) => Promise<T[]>,
    private maxBatchSize = 50
  ) {}
  
  request(params: any): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push({
        id: Math.random().toString(36),
        params,
        resolve,
        reject,
      });
      
      if (this.queue.length >= this.maxBatchSize) {
        this.flush();
      } else {
        this.scheduleFlush();
      }
    });
  }
  
  private scheduleFlush() {
    if (this.timer) return;
    
    this.timer = setTimeout(() => {
      this.flush();
    }, this.batchDelay);
  }
  
  private async flush() {
    if (this.timer) {
      clearTimeout(this.timer);
      this.timer = null;
    }
    
    if (this.queue.length === 0) return;
    
    const batch = this.queue.splice(0, this.maxBatchSize);
    const params = batch.map(req => req.params);
    
    try {
      const results = await this.batchFetcher(params);
      
      batch.forEach((req, index) => {
        req.resolve(results[index]);
      });
    } catch (error) {
      batch.forEach(req => {
        req.reject(error);
      });
    }
  }
}

// 用户批量获取
const userBatcher = new RequestBatcher<User>(
  async (userIds: string[]) => {
    const response = await apiClient.post('/users/batch', { ids: userIds });
    return response.data;
  }
);

// Hook使用批处理
function useUser(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => userBatcher.request(userId),
    staleTime: 5000,
  });
}

// DataLoader模式
class DataLoader<K, V> {
  private cache = new Map<K, Promise<V>>();
  private queue: Array<{
    key: K;
    resolve: (value: V) => void;
    reject: (error: any) => void;
  }> = [];
  
  constructor(
    private batchFn: (keys: K[]) => Promise<V[]>,
    private options: {
      maxBatchSize?: number;
      batchDelay?: number;
      cache?: boolean;
    } = {}
  ) {
    this.options = {
      maxBatchSize: 50,
      batchDelay: 10,
      cache: true,
      ...options,
    };
  }
  
  load(key: K): Promise<V> {
    // 检查缓存
    if (this.options.cache && this.cache.has(key)) {
      return this.cache.get(key)!;
    }
    
    const promise = new Promise<V>((resolve, reject) => {
      this.queue.push({ key, resolve, reject });
      
      if (this.queue.length >= (this.options.maxBatchSize || 50)) {
        this.dispatch();
      } else {
        setTimeout(() => this.dispatch(), this.options.batchDelay);
      }
    });
    
    if (this.options.cache) {
      this.cache.set(key, promise);
    }
    
    return promise;
  }
  
  private async dispatch() {
    if (this.queue.length === 0) return;
    
    const batch = this.queue.splice(0, this.options.maxBatchSize);
    const keys = batch.map(item => item.key);
    
    try {
      const values = await this.batchFn(keys);
      
      batch.forEach((item, index) => {
        item.resolve(values[index]);
      });
    } catch (error) {
      batch.forEach(item => {
        item.reject(error);
      });
    }
  }
  
  clear(key?: K) {
    if (key) {
      this.cache.delete(key);
    } else {
      this.cache.clear();
    }
  }
}

// 使用DataLoader
const userLoader = new DataLoader<string, User>(
  async (ids) => {
    const response = await apiClient.post('/users/batch', { ids });
    return response.data;
  },
  { cache: true, maxBatchSize: 100 }
);

function useUserWithLoader(userId: string) {
  return useQuery({
    queryKey: ['user', userId],
    queryFn: () => userLoader.load(userId),
  });
}

请求取消与超时

tsx
// utils/requestCancellation.ts
class CancellableRequest {
  private controllers = new Map<string, AbortController>();
  
  // 执行可取消请求
  async fetch<T>(
    key: string,
    fetcher: (signal: AbortSignal) => Promise<T>,
    timeout?: number
  ): Promise<T> {
    // 取消之前的请求
    this.cancel(key);
    
    const controller = new AbortController();
    this.controllers.set(key, controller);
    
    let timeoutId: NodeJS.Timeout | undefined;
    
    if (timeout) {
      timeoutId = setTimeout(() => {
        controller.abort();
      }, timeout);
    }
    
    try {
      const result = await fetcher(controller.signal);
      return result;
    } finally {
      if (timeoutId) clearTimeout(timeoutId);
      this.controllers.delete(key);
    }
  }
  
  // 取消特定请求
  cancel(key: string) {
    const controller = this.controllers.get(key);
    if (controller) {
      controller.abort();
      this.controllers.delete(key);
    }
  }
  
  // 取消所有请求
  cancelAll() {
    this.controllers.forEach(controller => controller.abort());
    this.controllers.clear();
  }
}

export const cancellableRequest = new CancellableRequest();

// React Query集成
function useSearchWithCancel(query: string) {
  return useQuery({
    queryKey: ['search', query],
    queryFn: ({ signal }) => {
      return cancellableRequest.fetch(
        `search-${query}`,
        (abortSignal) => apiClient.get('/search', {
          params: { q: query },
          signal: abortSignal,
        }),
        5000 // 5秒超时
      );
    },
    enabled: query.length > 0,
  });
}

// 自动取消Hook
function useAutoCancelQuery<T>(
  key: string,
  fetcher: (signal: AbortSignal) => Promise<T>,
  deps: any[]
) {
  useEffect(() => {
    return () => {
      cancellableRequest.cancel(key);
    };
  }, [key]);
  
  return useQuery({
    queryKey: [key, ...deps],
    queryFn: ({ signal }) => fetcher(signal),
  });
}

// 组件卸载时取消
function SearchComponent() {
  const [query, setQuery] = useState('');
  
  const { data } = useAutoCancelQuery(
    'search',
    (signal) => apiClient.get('/search', {
      params: { q: query },
      signal,
    }),
    [query]
  );
  
  return <div>...</div>;
}

数据一致性保障

tsx
// utils/dataConsistency.ts
class DataConsistencyManager {
  private queryClient: QueryClient;
  private subscriptions = new Map<string, Set<string>>();
  
  constructor(queryClient: QueryClient) {
    this.queryClient = queryClient;
  }
  
  // 注册数据依赖关系
  registerDependency(parent: string, child: string) {
    if (!this.subscriptions.has(parent)) {
      this.subscriptions.set(parent, new Set());
    }
    this.subscriptions.get(parent)!.add(child);
  }
  
  // 更新时级联失效
  invalidate(key: string) {
    // 失效自己
    this.queryClient.invalidateQueries({ queryKey: [key] });
    
    // 失效所有依赖项
    const dependents = this.subscriptions.get(key);
    if (dependents) {
      dependents.forEach(dependent => {
        this.queryClient.invalidateQueries({ queryKey: [dependent] });
      });
    }
  }
  
  // 乐观更新with回滚
  async optimisticUpdate<T>(
    key: string[],
    updater: (old: T) => T,
    mutationFn: () => Promise<T>
  ): Promise<T> {
    // 取消进行中的查询
    await this.queryClient.cancelQueries({ queryKey: key });
    
    // 保存快照
    const previousData = this.queryClient.getQueryData<T>(key);
    
    // 乐观更新
    this.queryClient.setQueryData<T>(key, (old) => {
      if (!old) return old;
      return updater(old);
    });
    
    try {
      const result = await mutationFn();
      
      // 成功后更新为服务器数据
      this.queryClient.setQueryData(key, result);
      
      return result;
    } catch (error) {
      // 失败时回滚
      this.queryClient.setQueryData(key, previousData);
      throw error;
    }
  }
  
  // 版本控制
  async updateWithVersion<T extends { version: number }>(
    key: string[],
    data: T,
    mutationFn: (data: T) => Promise<T>
  ): Promise<T> {
    const current = this.queryClient.getQueryData<T>(key);
    
    if (current && current.version !== data.version) {
      throw new Error('Version conflict - data has been modified');
    }
    
    const result = await mutationFn({
      ...data,
      version: (data.version || 0) + 1,
    });
    
    this.queryClient.setQueryData(key, result);
    
    return result;
  }
}

// 使用示例
const consistencyManager = new DataConsistencyManager(queryClient);

// 注册依赖
consistencyManager.registerDependency('user', 'user-posts');
consistencyManager.registerDependency('user', 'user-comments');

// 更新用户时自动失效相关数据
function useUpdateUser() {
  return useMutation({
    mutationFn: updateUserAPI,
    onSuccess: (data) => {
      consistencyManager.invalidate('user');
    },
  });
}

// 乐观更新
function useOptimisticLike(postId: string) {
  return useMutation({
    mutationFn: () => apiClient.post(`/posts/${postId}/like`),
    onMutate: async () => {
      const key = ['post', postId];
      
      return consistencyManager.optimisticUpdate<Post>(
        key,
        (old) => ({
          ...old,
          likes: old.likes + 1,
          isLiked: true,
        }),
        async () => {
          const response = await apiClient.post(`/posts/${postId}/like`);
          return response.data;
        }
      );
    },
  });
}

测试最佳实践

tsx
// __tests__/dataFetching.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

// Mock服务器
const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(ctx.json([
      { id: '1', name: 'John' },
      { id: '2', name: 'Jane' },
    ]));
  }),
  
  rest.get('/api/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    return res(ctx.json({ id, name: `User ${id}` }));
  }),
  
  rest.post('/api/users', async (req, res, ctx) => {
    const body = await req.json();
    return res(ctx.json({ id: '3', ...body }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// 测试wrapper
function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  });
  
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// 测试查询
describe('useUsers', () => {
  it('fetches users successfully', async () => {
    const { result } = renderHook(() => useUsers(), {
      wrapper: createWrapper(),
    });
    
    expect(result.current.isLoading).toBe(true);
    
    await waitFor(() => {
      expect(result.current.isSuccess).toBe(true);
    });
    
    expect(result.current.data).toHaveLength(2);
    expect(result.current.data[0].name).toBe('John');
  });
  
  it('handles errors correctly', async () => {
    server.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(ctx.status(500), ctx.json({ message: 'Server error' }));
      })
    );
    
    const { result } = renderHook(() => useUsers(), {
      wrapper: createWrapper(),
    });
    
    await waitFor(() => {
      expect(result.current.isError).toBe(true);
    });
    
    expect(result.current.error).toBeDefined();
  });
});

// 测试Mutation
describe('useCreateUser', () => {
  it('creates user and invalidates cache', async () => {
    const queryClient = new QueryClient();
    const wrapper = createWrapper();
    
    const { result } = renderHook(
      () => ({
        users: useUsers(),
        createUser: useCreateUser(),
      }),
      { wrapper }
    );
    
    // 等待初始加载
    await waitFor(() => {
      expect(result.current.users.isSuccess).toBe(true);
    });
    
    // 创建用户
    await act(async () => {
      await result.current.createUser.mutateAsync({
        name: 'New User',
        email: 'new@example.com',
      });
    });
    
    // 验证缓存已失效并重新获取
    await waitFor(() => {
      expect(result.current.users.data).toHaveLength(2);
    });
  });
});

// 测试乐观更新
describe('optimistic updates', () => {
  it('updates UI immediately and rolls back on error', async () => {
    let shouldFail = false;
    
    server.use(
      rest.post('/api/posts/:id/like', (req, res, ctx) => {
        if (shouldFail) {
          return res(ctx.status(500));
        }
        return res(ctx.json({ success: true }));
      })
    );
    
    const { result } = renderHook(() => useOptimisticLike('1'), {
      wrapper: createWrapper(),
    });
    
    // 触发乐观更新
    act(() => {
      result.current.mutate();
    });
    
    // UI立即更新
    // ... 验证UI状态
    
    // 设置失败
    shouldFail = true;
    
    // 再次触发
    act(() => {
      result.current.mutate();
    });
    
    // 等待错误
    await waitFor(() => {
      expect(result.current.isError).toBe(true);
    });
    
    // 验证已回滚
    // ... 验证UI状态已恢复
  });
});

性能监控Dashboard

tsx
// components/PerformanceMonitor.tsx
import { useQuery } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';

interface QueryMetrics {
  key: string;
  fetchCount: number;
  cacheHits: number;
  cacheMisses: number;
  averageFetchTime: number;
  errorRate: number;
}

function PerformanceMonitor() {
  const queryClient = useQueryClient();
  const [metrics, setMetrics] = useState<QueryMetrics[]>([]);
  
  useEffect(() => {
    const interval = setInterval(() => {
      const cache = queryClient.getQueryCache();
      const queries = cache.getAll();
      
      const newMetrics: QueryMetrics[] = queries.map(query => {
        const state = query.state;
        const meta = query.meta as any;
        
        return {
          key: JSON.stringify(query.queryKey),
          fetchCount: meta?.fetchCount || 0,
          cacheHits: meta?.cacheHits || 0,
          cacheMisses: meta?.cacheMisses || 0,
          averageFetchTime: meta?.averageFetchTime || 0,
          errorRate: meta?.errorRate || 0,
        };
      });
      
      setMetrics(newMetrics);
    }, 1000);
    
    return () => clearInterval(interval);
  }, [queryClient]);
  
  const totalRequests = metrics.reduce((sum, m) => sum + m.fetchCount, 0);
  const cacheHitRate = metrics.reduce((sum, m) => {
    const total = m.cacheHits + m.cacheMisses;
    return sum + (total > 0 ? m.cacheHits / total : 0);
  }, 0) / metrics.length || 0;
  
  return (
    <div className="performance-monitor">
      <h3>Performance Metrics</h3>
      
      <div className="summary">
        <div>Total Requests: {totalRequests}</div>
        <div>Cache Hit Rate: {(cacheHitRate * 100).toFixed(1)}%</div>
        <div>Active Queries: {metrics.length}</div>
      </div>
      
      <table>
        <thead>
          <tr>
            <th>Query Key</th>
            <th>Fetches</th>
            <th>Cache Hits</th>
            <th>Avg Time (ms)</th>
            <th>Error Rate</th>
          </tr>
        </thead>
        <tbody>
          {metrics.map((metric, index) => (
            <tr key={index}>
              <td>{metric.key}</td>
              <td>{metric.fetchCount}</td>
              <td>{metric.cacheHits}</td>
              <td>{metric.averageFetchTime.toFixed(0)}</td>
              <td>{(metric.errorRate * 100).toFixed(1)}%</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

最佳实践检查清单

数据获取最佳实践检查清单:

架构层面:
☐ API层统一封装
☐ 服务层明确划分
☐ Hook层合理抽象
☐ 类型定义完整
☐ 错误处理统一

性能优化:
☐ 请求去重机制
☐ 智能缓存策略
☐ 预加载关键数据
☐ 虚拟滚动大列表
☐ 请求优先级管理
☐ 批量请求合并
☐ 取消无用请求

用户体验:
☐ Loading状态优雅
☐ 错误提示友好
☐ 乐观更新流畅
☐ 离线支持完善
☐ 骨架屏占位

安全性:
☐ Token自动刷新
☐ 请求加密(HTTPS)
☐ 数据验证(Zod)
☐ XSS防护
☐ CSRF防护

监控调试:
☐ 请求日志记录
☐ 性能指标追踪
☐ 错误上报配置
☐ DevTools集成
☐ 单元测试覆盖

可维护性:
☐ 代码注释充分
☐ 文档更新及时
☐ 版本控制规范
☐ Code Review流程
☐ 技术债务管理

数据获取是React应用的基础,遵循这些最佳实践可以构建高性能、安全、可维护的应用程序。