在前端开发中,请求状态管理看似简单却暗藏玄机。你是否经常遇到这些情况?
- 列表加载时忘记显示 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 等库提供了优秀方案,但它们在以下场景仍有不足:
- 复杂状态流转:多个请求的依赖关系
- 乐观更新回滚:失败时的状态恢复
- 请求优先级:关键请求优先处理
- 离线队列:网络恢复后自动重试
二、设计思路:状态机驱动的请求管理
2.1 核心思想:请求作为状态机
每个请求都可以看作一个状态机:
┌───────────┐
│ IDLE │
└─────┬─────┘
│
┌─────▼─────┐
│ LOADING │
└─────┬─────┘
┌──────┴──────┐
┌──────▼─────┐ ┌────▼──────┐
│ SUCCESS │ │ ERROR │
└────────────┘ └───────────┘
2.2 智能请求的特征
- 自包含状态:每个请求自带 loading、error、data 状态
- 自动取消:组件卸载时自动取消未完成请求
- 依赖管理:处理请求间的依赖关系
- 缓存智能:基于 stale-while-revalidate 策略
- 重试机制:可配置的指数退避重试
三、实现方案:从理论到实践
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 核心优势
- 状态集中管理:告别分散的 loading/error 状态
- 智能缓存:基于 stale-while-revalidate 策略
- 自动清理:防止内存泄漏
- 竞态安全:确保数据一致性
- 开发体验:简化复杂状态流转
6.2 扩展方向
未来可以进一步探索:
- 离线优先:Service Worker 集成
- 请求优先级:关键请求优先处理
- 预请求:根据用户行为预测并预加载
- 请求分析:性能监控与优化建议
七、实践建议
- 渐进式采用:从复杂场景开始,逐步推广
- 统一规范:团队制定统一的请求管理规范
- 监控告警:对异常请求模式进行监控
- 文档沉淀:记录最佳实践和常见问题
你在项目中遇到过哪些请求管理的痛点?采用了什么解决方案?欢迎在评论区分享你的经验和想法!
如果你觉得这篇文章有帮助,请点赞收藏支持!