Appearance
数据获取最佳实践
概述
数据获取是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')
);
}总结
数据获取最佳实践核心要点:
- 架构设计:API层抽象、服务层封装
- 错误处理:统一处理、错误边界
- 性能优化:请求去重、预加载、虚拟滚动
- 缓存策略:智能缓存时间、失效策略
- 类型安全:严格类型定义、泛型封装
- 安全性:认证管理、数据验证
- 监控调试:请求日志、性能监控
遵循这些最佳实践可以构建高性能、可维护的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应用的基础,遵循这些最佳实践可以构建高性能、安全、可维护的应用程序。