告别混乱:智能请求状态管理实践与思考

25 阅读5分钟

在前端开发中,请求状态管理看似简单却暗藏玄机。你是否经常遇到这些情况?

  • 列表加载时忘记显示 loading 状态
  • 提交表单后用户重复点击导致重复提交
  • 页面切换时,未完成的请求未正确取消
  • 错误处理逻辑分散在各个组件中
  • 缓存策略混乱,时而新鲜时而过时

今天,我将分享一种智能请求状态管理方案,它结合了 状态机思想 与 声明式编程,让你的应用状态如丝般顺滑。

一、问题分析:传统请求管理的痛点

1.1 常见的“土法炼钢”

// 典型的“土法炼钢”式请求管理
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)

const fetchData = async () => {
  setLoading(true)
  setError(null)
  try {
    const res = await api.getData()
    setData(res)
  } catch (err) {
    setError(err)
  } finally {
    setLoading(false)
  }
}

这种模式的问题:

  • 状态分散:每个请求都需要维护多个状态
  • 竞态条件:快速切换页面可能导致旧请求覆盖新结果
  • 生命周期管理:组件卸载后 setState 警告

1.2 现有解决方案的局限

虽然 React Query、SWR 等库提供了优秀方案,但它们在以下场景仍有不足:

  1. 复杂状态流转:多个请求的依赖关系
  2. 乐观更新回滚:失败时的状态恢复
  3. 请求优先级:关键请求优先处理
  4. 离线队列:网络恢复后自动重试

二、设计思路:状态机驱动的请求管理

2.1 核心思想:请求作为状态机

每个请求都可以看作一个状态机:

        ┌───────────┐
        │  IDLE     │
        └─────┬─────┘
              │
        ┌─────▼─────┐
        │  LOADING  │
        └─────┬─────┘
       ┌──────┴──────┐
┌──────▼─────┐ ┌────▼──────┐
│   SUCCESS  │ │   ERROR   │
└────────────┘ └───────────┘

2.2 智能请求的特征

  1. 自包含状态:每个请求自带 loading、error、data 状态
  2. 自动取消:组件卸载时自动取消未完成请求
  3. 依赖管理:处理请求间的依赖关系
  4. 缓存智能:基于 stale-while-revalidate 策略
  5. 重试机制:可配置的指数退避重试

三、实现方案:从理论到实践

3.1 基础 Hook 实现

import { useRef, useState, useCallback, useEffect } from 'react';

type RequestState<T> = {
  data: T | null;
  error: Error | null;
  status: 'idle' | 'loading' | 'success' | 'error';
  timestamp: number;
};

type UseRequestOptions<T, P extends any[]> = {
  manual?: boolean;
  defaultParams?: P;
  refreshDeps?: any[];
  onSuccess?: (data: T, params: P) => void;
  onError?: (error: Error, params: P) => void;
  cacheTime?: number;
  staleTime?: number;
};

export function useRequest<T, P extends any[] = any[]>(
  fetcher: (...args: P) => Promise<T>,
  options: UseRequestOptions<T, P> = {}
) {
  const {
    manual = false,
    defaultParams = [] as unknown as P,
    refreshDeps = [],
    cacheTime = 5 * 60 * 1000, // 5分钟
    staleTime = 0,
  } = options;

  const [state, setState] = useState<RequestState<T>>({
    data: null,
    error: null,
    status: 'idle',
    timestamp: 0,
  });

  const abortControllerRef = useRef<AbortController | null>(null);
  const cacheRef = useRef<Map<string, { data: T; timestamp: number }>>(new Map());

  // 生成缓存键
  const generateCacheKey = useCallback((params: P): string => {
    return JSON.stringify(params);
  }, []);

  // 检查缓存是否新鲜
  const isCacheFresh = useCallback((cacheKey: string): boolean => {
    const cached = cacheRef.current.get(cacheKey);
    if (!cached) return false;
    const now = Date.now();
    return now - cached.timestamp < staleTime;
  }, [staleTime]);

  // 检查缓存是否有效(未过期)
  const isCacheValid = useCallback((cacheKey: string): boolean => {
    const cached = cacheRef.current.get(cacheKey);
    if (!cached) return false;
    const now = Date.now();
    return now - cached.timestamp < cacheTime;
  }, [cacheTime]);

  const run = useCallback(async (...params: P) => {
    // 取消之前的请求
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    const cacheKey = generateCacheKey(params);
    
    // 如果缓存新鲜,直接使用
    if (isCacheFresh(cacheKey)) {
      const cached = cacheRef.current.get(cacheKey)!;
      setState({
        data: cached.data,
        error: null,
        status: 'success',
        timestamp: cached.timestamp,
      });
      return cached.data;
    }

    // 如果有有效缓存,先显示缓存数据(stale-while-revalidate)
    if (isCacheValid(cacheKey)) {
      const cached = cacheRef.current.get(cacheKey)!;
      setState(prev => ({
        ...prev,
        data: cached.data,
        status: 'loading', // 状态变为 loading,但已有缓存数据
      }));
    } else {
      setState({
        data: null,
        error: null,
        status: 'loading',
        timestamp: Date.now(),
      });
    }

    // 创建新的 AbortController
    abortControllerRef.current = new AbortController();

    try {
      const data = await fetcher(...params, {
        signal: abortControllerRef.current.signal,
      });

      // 更新缓存
      const timestamp = Date.now();
      cacheRef.current.set(cacheKey, { data, timestamp });

      setState({
        data,
        error: null,
        status: 'success',
        timestamp,
      });

      options.onSuccess?.(data, params);
      return data;
    } catch (error) {
      // 如果是取消请求,不更新错误状态
      if (error instanceof DOMException && error.name === 'AbortError') {
        return;
      }

      setState(prev => ({
        ...prev,
        error: error as Error,
        status: 'error',
      }));

      options.onError?.(error as Error, params);
      throw error;
    }
  }, [fetcher, options.onSuccess, options.onError, generateCacheKey, isCacheFresh, isCacheValid]);

  // 自动执行(非手动模式)
  useEffect(() => {
    if (!manual) {
      run(...defaultParams);
    }
  }, [...refreshDeps, manual]);

  // 清理函数
  useEffect(() => {
    return () => {
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
      
      // 清理过期缓存
      const now = Date.now();
      for (const [key, cached] of cacheRef.current.entries()) {
        if (now - cached.timestamp > cacheTime) {
          cacheRef.current.delete(key);
        }
      }
    };
  }, [cacheTime]);

  return {
    ...state,
    run,
    mutate: (newData: T | ((oldData: T | null) => T)) => {
      setState(prev => ({
        ...prev,
        data: typeof newData === 'function' 
          ? (newData as Function)(prev.data)
          : newData,
      }));
    },
    isFresh: () => {
      const now = Date.now();
      return now - state.timestamp < staleTime;
    },
  };
}

3.2 高级特性:请求依赖与队列

// 请求依赖管理器
class RequestDependencyManager {
  private dependencies = new Map<string, string[]>();
  private runningRequests = new Set<string>();
  private pendingQueue = new Map<string, (() => Promise<any>)[]>();

  addDependency(requestId: string, dependsOn: string[]) {
    this.dependencies.set(requestId, dependsOn);
  }

  async execute(requestId: string, executor: () => Promise<any>) {
    const deps = this.dependencies.get(requestId) || [];
    
    // 检查依赖是否都已完成
    const unmetDeps = deps.filter(dep => this.runningRequests.has(dep));
    
    if (unmetDeps.length > 0) {
      // 有未完成的依赖,加入等待队列
      if (!this.pendingQueue.has(requestId)) {
        this.pendingQueue.set(requestId, []);
      }
      
      return new Promise((resolve, reject) => {
        const pendingExecutor = async () => {
          try {
            const result = await executor();
            resolve(result);
          } catch (error) {
            reject(error);
          }
        };
        this.pendingQueue.get(requestId)!.push(pendingExecutor);
      });
    }
    
    // 没有未完成的依赖,直接执行
    this.runningRequests.add(requestId);
    try {
      const result = await executor();
      this.runningRequests.delete(requestId);
      
      // 检查是否有等待当前请求的后续请求
      this.checkPendingRequests(requestId);
      
      return result;
    } catch (error) {
      this.runningRequests.delete(requestId);
      this.checkPendingRequests(requestId);
      throw error;
    }
  }

  private checkPendingRequests(completedRequestId: string) {
    // 找出所有依赖已完成请求的待执行请求
    for (const [requestId, executors] of this.pendingQueue.entries()) {
      const deps = this.dependencies.get(requestId) || [];
      
      // 检查是否所有依赖都已完成
      const allDepsMet = !deps.some(dep => this.runningRequests.has(dep));
      
      if (allDepsMet && executors.length > 0) {
        // 执行所有等待的执行器
        executors.forEach(executor => executor());
        this.pendingQueue.delete(requestId);
      }
    }
  }
}

3.3 乐观更新与回滚

// 乐观更新 Hook
function useOptimisticUpdate<T>(initialData: T) {
  const [data, setData] = useState(initialData);
  const [history, setHistory] = useState<T[]>([]);
  const [isUpdating, setIsUpdating] = useState(false);

  const optimisticUpdate = useCallback(
    async (updateFn: (current: T) => T, asyncAction: () => Promise<any>) => {
      // 保存当前状态到历史记录
      setHistory(prev => [...prev, data]);
      
      // 立即应用乐观更新
      const newData = updateFn(data);
      setData(newData);
      setIsUpdating(true);

      try {
        await asyncAction();
        setIsUpdating(false);
        // 成功后清除历史记录
        setHistory([]);
      } catch (error) {
        // 失败时回滚到上一个状态
        if (history.length > 0) {
          const previousData = history[history.length - 1];
          setData(previousData);
          setHistory(prev => prev.slice(0, -1));
        }
        setIsUpdating(false);
        throw error;
      }
    },
    [data, history]
  );

  return { data, optimisticUpdate, isUpdating };
}

四、实战应用:复杂场景解决方案

4.1 场景一:表单提交防重与乐观更新

function SubmitButton({ onSubmit }: { onSubmit: () => Promise<void> }) {
  const { run, status } = useRequest(onSubmit, {
    manual: true,
    onSuccess: () => {
      message.success('提交成功');
    },
    onError: (error) => {
      message.error(`提交失败: ${error.message}`);
    },
  });

  return (
    <Button
      type="primary"
      loading={status === 'loading'}
      disabled={status === 'loading'}
      onClick={() => run()}
      // 额外防重:防止双击
      onDoubleClick={(e) => e.preventDefault()}
    >
      {status === 'loading' ? '提交中...' : '提交'}
    </Button>
  );
}

4.2 场景二:列表的无限滚动与缓存

function useInfiniteList<T>(fetcher: (page: number) => Promise<T[]>) {
  const [page, setPage] = useState(1);
  const [list, setList] = useState<T[]>([]);
  const [hasMore, setHasMore] = useState(true);

  const { run, status } = useRequest(
    async (currentPage: number) => {
      const data = await fetcher(currentPage);
      return data;
    },
    {
      manual: true,
      onSuccess: (data, [currentPage]) => {
        if (currentPage === 1) {
          setList(data);
        } else {
          setList(prev => [...prev, ...data]);
        }
        
        // 假设空数组表示没有更多数据
        setHasMore(data.length > 0);
      },
    }
  );

  const loadMore = useCallback(() => {
    if (hasMore && status !== 'loading') {
      run(page);
      setPage(p => p + 1);
    }
  }, [hasMore, status, page, run]);

  // 初始加载
  useEffect(() => {
    run(1);
  }, []);

  return { list, loadMore, hasMore, loading: status === 'loading' };
}

4.3 场景三:竞态条件处理

function useRaceConditionSafeRequest<T, P extends any[]>(
  fetcher: (...args: P) => Promise<T>,
  requestId: string
) {
  const latestRequestId = useRef<string | null>(null);

  const safeFetch = useCallback(async (...args: P) => {
    const currentRequestId = requestId + Date.now();
    latestRequestId.current = currentRequestId;

    const result = await fetcher(...args);

    // 检查是否是最新的请求
    if (latestRequestId.current === currentRequestId) {
      return result;
    }
    
    // 如果不是最新请求,抛出特定错误
    throw new Error('Request outdated');
  }, [fetcher, requestId]);

  return useRequest(safeFetch);
}

五、性能优化与调试

5.1 缓存策略调优

// 基于业务场景的缓存策略
const cacheStrategies = {
  // 用户配置:短时间缓存,需要及时更新
  userProfile: {
    staleTime: 30 * 1000, // 30秒后视为过期
    cacheTime: 5 * 60 * 1000, // 缓存5分钟
  },
  
  // 静态数据:长时间缓存
  staticData: {
    staleTime: 24 * 60 * 60 * 1000, // 24小时
    cacheTime: 7 * 24 * 60 * 60 * 1000, // 7天
  },
  
  // 实时数据:不缓存
  realtime: {
    staleTime: 0,
    cacheTime: 0,
  }
};

5.2 请求监控与调试面板

// 开发环境下的请求监控
if (process.env.NODE_ENV === 'development') {
  const requestLogger = {
    log: (type: string, ...args: any[]) => {
      console.groupCollapsed(`[Request ${type}]`);
      console.log(...args);
      console.groupEnd();
    }
  };

  // 在 useRequest 中添加监控
  const monitoredFetcher = async (...args: any[]) => {
    requestLogger.log('Start', { args, timestamp: Date.now() });
    
    try {
      const result = await fetcher(...args);
      requestLogger.log('Success', { result, timestamp: Date.now() });
      return result;
    } catch (error) {
      requestLogger.log('Error', { error, timestamp: Date.now() });
      throw error;
    }
  };
}

六、总结与展望

6.1 核心优势

  1. 状态集中管理:告别分散的 loading/error 状态
  2. 智能缓存:基于 stale-while-revalidate 策略
  3. 自动清理:防止内存泄漏
  4. 竞态安全:确保数据一致性
  5. 开发体验:简化复杂状态流转

6.2 扩展方向

未来可以进一步探索:

  1. 离线优先:Service Worker 集成
  2. 请求优先级:关键请求优先处理
  3. 预请求:根据用户行为预测并预加载
  4. 请求分析:性能监控与优化建议

七、实践建议

  1. 渐进式采用:从复杂场景开始,逐步推广
  2. 统一规范:团队制定统一的请求管理规范
  3. 监控告警:对异常请求模式进行监控
  4. 文档沉淀:记录最佳实践和常见问题

你在项目中遇到过哪些请求管理的痛点?采用了什么解决方案?欢迎在评论区分享你的经验和想法!

如果你觉得这篇文章有帮助,请点赞收藏支持!