跳到主要内容

最佳实践

本文档总结了使用 Hook-Fetch 的最佳实践,帮助您构建高效、可维护的应用程序。

项目结构

推荐的项目结构

src/
├── api/
│ ├── index.ts # API 实例配置
│ ├── endpoints.ts # 端点定义
│ ├── types.ts # 类型定义
│ └── plugins/ # 自定义插件
│ ├── auth.ts
│ ├── logger.ts
│ └── retry.ts
├── hooks/
│ ├── useApi.ts # 通用 API Hook
│ ├── useAuth.ts # 认证相关 Hook
│ └── useStream.ts # 流式数据 Hook
└── utils/
├── constants.ts # 常量定义
└── helpers.ts # 辅助函数

API 实例配置

// src/api/index.ts
import hookFetch from 'hook-fetch';
import { authPlugin } from './plugins/auth';
import { loggerPlugin } from './plugins/logger';
import { retryPlugin } from './plugins/retry';

// 创建主 API 实例
export const api = hookFetch.create({
baseURL: process.env.REACT_APP_API_URL || 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
},
plugins: [
authPlugin(),
retryPlugin({ maxRetries: 3, delay: 1000 }),
loggerPlugin()
]
});

// 创建专门的流式 API 实例
export const streamApi = hookFetch.create({
baseURL: process.env.REACT_APP_STREAM_URL || 'https://stream.example.com',
plugins: [
authPlugin(),
sseTextDecoderPlugin({
json: true,
prefix: 'data: ',
doneSymbol: '[DONE]'
})
]
});

端点定义

// src/api/endpoints.ts
export const endpoints = {
// 用户相关
users: {
list: '/users',
detail: (id: string) => `/users/${id}`,
create: '/users',
update: (id: string) => `/users/${id}`,
delete: (id: string) => `/users/${id}`
},

// 认证相关
auth: {
login: '/auth/login',
logout: '/auth/logout',
refresh: '/auth/refresh',
profile: '/auth/profile'
},

// 流式端点
stream: {
chat: '/stream/chat',
logs: '/stream/logs',
metrics: '/stream/metrics'
}
} as const;

类型定义

// src/api/types.ts
export interface User {
id: string;
name: string;
email: string;
createdAt: string;
updatedAt: string;
}

export interface ApiResponse<T = any> {
data: T;
message: string;
success: boolean;
code: number;
}

export interface PaginatedResponse<T = any> {
data: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}

export interface StreamMessage {
id: string;
content: string;
timestamp: number;
type: 'user' | 'assistant' | 'system';
}

错误处理

全局错误处理

// src/api/plugins/error.ts
export function errorHandlerPlugin() {
return {
name: 'error-handler',
async onError(error, config) {
// 记录错误
console.error(`[API Error] ${config.method} ${config.url}:`, error);

// 根据错误类型处理
if (error.response) {
const status = error.status;

switch (status) {
case 401:
// 未授权,重定向到登录页
window.location.href = '/login';
break;
case 403:
// 权限不足
showNotification('权限不足', 'error');
break;
case 404:
// 资源不存在
showNotification('请求的资源不存在', 'error');
break;
case 500:
// 服务器错误
showNotification('服务器内部错误,请稍后重试', 'error');
break;
default:
showNotification('请求失败,请检查网络连接', 'error');
}
}
else if (error.name === 'AbortError') {
// 请求被取消,通常不需要显示错误
console.log('Request was aborted');
}
else {
// 网络错误或其他错误
showNotification('网络连接失败,请检查网络设置', 'error');
}

return error;
}
};
}

组件级错误处理

// src/hooks/useApi.ts
import { useHookFetch } from 'hook-fetch/react';
import { api } from '../api';

export function useApi() {
const { request, loading, cancel } = useHookFetch({
request: api.request,
onError: (error) => {
// 组件级错误处理
if (error.response?.status === 422) {
// 表单验证错误
return error.response.json().then((data) => {
showValidationErrors(data.errors);
});
}
}
});

return {
request,
loading,
cancel,
// 封装常用方法
get: (url: string, params?: any) =>
request(url, { method: 'GET', params }).json(),
post: (url: string, data?: any) =>
request(url, { method: 'POST', data }).json(),
put: (url: string, data?: any) =>
request(url, { method: 'PUT', data }).json(),
delete: (url: string) =>
request(url, { method: 'DELETE' }).json()
};
}

性能优化

请求缓存

// src/api/plugins/cache.ts
export function cachePlugin(options = {}) {
const defaultOptions = {
ttl: 5 * 60 * 1000, // 5分钟
maxSize: 100,
excludeMethods: ['POST', 'PUT', 'PATCH', 'DELETE']
};

const config = { ...defaultOptions, ...options };
const cache = new Map();

const getRequestKey = (url: string, method: string, params: any, data: any) => {
return `${url}::${method}::${JSON.stringify(params)}::${JSON.stringify(data)}`;
};

return {
name: 'cache',
async beforeRequest(requestConfig) {
if (config.excludeMethods.includes(requestConfig.method)) {
return requestConfig;
}

const key = getRequestKey(
requestConfig.url,
requestConfig.method,
requestConfig.params,
requestConfig.data
);
const cached = cache.get(key);

if (cached) {
// 检查缓存是否过期
if (cached.timestamp + config.ttl > Date.now()) {
// 返回缓存数据,使用 resolve 属性
return {
...requestConfig,
resolve: () => new Response(JSON.stringify(cached.data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
};
}
else {
// 缓存已过期,删除缓存
cache.delete(key);
}
}

return requestConfig;
},
async afterResponse(context, requestConfig) {
if (config.excludeMethods.includes(requestConfig.method)) {
return context;
}

const key = getRequestKey(
requestConfig.url,
requestConfig.method,
requestConfig.params,
requestConfig.data
);

// 限制缓存大小
if (cache.size >= config.maxSize) {
const firstKey = cache.keys().next().value;
cache.delete(firstKey);
}

// 缓存响应
cache.set(key, {
data: context.result,
timestamp: Date.now()
});

return context;
}
};
}

请求去重

官方不推荐使用

虽然我们提供了请求去重插件,但官方并不推荐在生产环境中使用。去重逻辑会增加系统复杂度,可能导致意外的行为。建议在应用层面通过设计来避免重复请求:

  • 禁用按钮防止重复点击
  • 使用防抖/节流处理用户输入
  • 使用请求状态管理避免并发请求

如果确实需要使用去重功能,请使用官方提供的插件,但请谨慎使用。

// 使用官方去重插件
import { dedupePlugin, isDedupeError } from 'hook-fetch/plugins/dedupe';

const api = hookFetch.create({
baseURL: 'https://api.example.com',
plugins: [dedupePlugin()]
});

// 并发相同请求会被去重
try {
const promises = [
api.get('/users/1').json(),
api.get('/users/1').json(), // 会被去重
];

const results = await Promise.allSettled(promises);
results.forEach((result, index) => {
if (result.status === 'rejected' && isDedupeError(result.reason)) {
console.log(`请求 ${index + 1} 被去重`);
}
else if (result.status === 'fulfilled') {
console.log(`请求 ${index + 1} 成功:`, result.value);
}
});
}
catch (error) {
if (isDedupeError(error)) {
console.log('检测到重复请求');
}
}

// 禁用特定请求的去重
const response = await api.get('/users/1', {}, {
extra: { dedupeAble: false }
}).json();

更好的替代方案:

// 推荐:使用防抖避免重复提交
import { debounce } from 'lodash-es';

const handleSubmit = debounce(async (data) => {
await api.post('/users', data).json();
}, 300);

// 推荐:使用按钮禁用状态
function UserForm() {
const [isSubmitting, setIsSubmitting] = useState(false);

const handleSubmit = async (data) => {
if (isSubmitting) return;

setIsSubmitting(true);
try {
await api.post('/users', data).json();
}
finally {
setIsSubmitting(false);
}
};

return (
<button disabled={isSubmitting} onClick={handleSubmit}>
{isSubmitting ? '提交中...' : '提交'}
</button>
);
}

批量请求

// src/utils/batch.ts
export class BatchRequestManager {
private batchQueue: Array<{
url: string;
params: any;
resolve: (data: any) => void;
reject: (error: any) => void;
}> = [];

private batchTimeout: NodeJS.Timeout | null = null;

constructor(
private api: any,
private batchSize = 10,
private delay = 100
) {}

async request(url: string, params: any): Promise<any> {
return new Promise((resolve, reject) => {
this.batchQueue.push({ url, params, resolve, reject });

if (this.batchQueue.length >= this.batchSize) {
this.processBatch();
}
else if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => {
this.processBatch();
}, this.delay);
}
});
}

private async processBatch() {
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}

const currentBatch = this.batchQueue.splice(0, this.batchSize);

try {
const batchRequest = {
requests: currentBatch.map(({ url, params }) => ({ url, params }))
};

const response = await this.api.post('/batch', batchRequest);

response.results.forEach((result: any, index: number) => {
const { resolve, reject } = currentBatch[index];
if (result.success) {
resolve(result.data);
}
else {
reject(new Error(result.error));
}
});
}
catch (error) {
currentBatch.forEach(({ reject }) => reject(error));
}
}
}

流式数据处理

流式数据管理

import { useHookFetch } from 'hook-fetch/react';
// src/hooks/useStream.ts
import { useCallback, useRef } from 'react';

export function useStream<T = any>(requestFn: (...args: any[]) => any) {
const dataRef = useRef<T[]>([]);
const listenersRef = useRef<Set<(data: T[]) => void>>(new Set());

const { stream, loading, cancel } = useHookFetch({
request: requestFn,
onError: (error) => {
console.error('Stream error:', error);
}
});

const subscribe = useCallback((listener: (data: T[]) => void) => {
listenersRef.current.add(listener);

return () => {
listenersRef.current.delete(listener);
};
}, []);

const startStream = useCallback(async (...args: any[]) => {
dataRef.current = [];

try {
for await (const chunk of stream(...args)) {
if (chunk.result) {
dataRef.current.push(chunk.result);

// 通知所有监听者
listenersRef.current.forEach((listener) => {
listener([...dataRef.current]);
});
}
}
}
catch (error) {
console.error('Stream processing error:', error);
}
}, [stream]);

const clear = useCallback(() => {
dataRef.current = [];
listenersRef.current.forEach((listener) => {
listener([]);
});
}, []);

return {
startStream,
subscribe,
clear,
cancel,
loading,
data: dataRef.current
};
}

流式数据缓冲

// src/utils/streamBuffer.ts
export class StreamBuffer<T> {
private buffer: T[] = [];
private flushTimeout: NodeJS.Timeout | null = null;

constructor(
private onFlush: (items: T[]) => void,
private bufferSize = 10,
private flushInterval = 1000
) {}

add(item: T) {
this.buffer.push(item);

if (this.buffer.length >= this.bufferSize) {
this.flush();
}
else if (!this.flushTimeout) {
this.flushTimeout = setTimeout(() => {
this.flush();
}, this.flushInterval);
}
}

flush() {
if (this.flushTimeout) {
clearTimeout(this.flushTimeout);
this.flushTimeout = null;
}

if (this.buffer.length > 0) {
this.onFlush([...this.buffer]);
this.buffer = [];
}
}

clear() {
this.buffer = [];
if (this.flushTimeout) {
clearTimeout(this.flushTimeout);
this.flushTimeout = null;
}
}
}

测试

单元测试

// src/api/__tests__/api.test.ts
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { api } from '../index';

// 模拟 fetch
global.fetch = vi.fn();

describe('API', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should make GET request', async () => {
const mockResponse = { data: { id: 1, name: 'John' } };

(fetch as any).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});

const result = await api.get('/users/1').json();

expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/users/1'),
expect.objectContaining({
method: 'GET'
})
);

expect(result).toEqual(mockResponse);
});

it('should handle errors', async () => {
(fetch as any).mockRejectedValueOnce(new Error('Network error'));

await expect(api.get('/users/1').json()).rejects.toThrow('Network error');
});
});

集成测试

// src/hooks/__tests__/useApi.test.ts
import { act, renderHook } from '@testing-library/react';
import { useApi } from '../useApi';

describe('useApi', () => {
it('should handle loading state', async () => {
const { result } = renderHook(() => useApi());

expect(result.current.loading).toBe(false);

act(() => {
result.current.get('/users');
});

expect(result.current.loading).toBe(true);
});
});

安全性

认证和授权

// src/api/plugins/auth.ts
export function authPlugin() {
return {
name: 'auth',
priority: 1,
async beforeRequest(config) {
const token = localStorage.getItem('authToken');

if (token) {
config.headers = new Headers(config.headers);
config.headers.set('Authorization', `Bearer ${token}`);
}

return config;
},
async onError(error) {
if (error.response?.status === 401) {
// Token 过期,提示用户重新登录
console.error('认证失败,请重新登录');
localStorage.removeItem('authToken');

// 可以在这里发出事件或导航到登录页
// 注意:在插件中直接修改 window.location 可能不是最佳实践
// 建议使用事件系统通知应用层处理

// 方式1:使用自定义事件
window.dispatchEvent(new CustomEvent('auth:logout'));

// 方式2:或者直接跳转(不推荐在插件中使用)
// window.location.href = '/login';
}

return error;
}
};
}

请求签名

// src/api/plugins/signature.ts
import { createHmac } from 'node:crypto';

export function signaturePlugin(secretKey: string) {
return {
name: 'signature',
async beforeRequest(config) {
const timestamp = Date.now().toString();
const nonce = Math.random().toString(36).substring(2);

// 创建签名字符串
const signString = `${config.method}${config.url}${timestamp}${nonce}`;
const signature = createHmac('sha256', secretKey)
.update(signString)
.digest('hex');

config.headers = new Headers(config.headers);
config.headers.set('X-Timestamp', timestamp);
config.headers.set('X-Nonce', nonce);
config.headers.set('X-Signature', signature);

return config;
}
};
}

监控和调试

性能监控

// src/api/plugins/performance.ts
export function performancePlugin() {
return {
name: 'performance',
async beforeRequest(config) {
config.extra = {
...config.extra,
startTime: performance.now()
};
return config;
},
async afterResponse(context, config) {
const endTime = performance.now();
const duration = endTime - (config.extra?.startTime || 0);

// 记录性能指标
console.log(`[Performance] ${config.method} ${config.url}: ${duration.toFixed(2)}ms`);

// 发送到监控系统
if (duration > 5000) { // 超过5秒的请求
sendToMonitoring({
type: 'slow_request',
url: config.url,
method: config.method,
duration
});
}

return context;
}
};
}

调试工具

// src/utils/debug.ts
export function createDebugger(namespace: string) {
const isDebugEnabled = process.env.NODE_ENV === 'development'
|| localStorage.getItem('debug') === 'true';

return {
log: (...args: any[]) => {
if (isDebugEnabled) {
console.log(`[${namespace}]`, ...args);
}
},
warn: (...args: any[]) => {
if (isDebugEnabled) {
console.warn(`[${namespace}]`, ...args);
}
},
error: (...args: any[]) => {
if (isDebugEnabled) {
console.error(`[${namespace}]`, ...args);
}
}
};
}

部署和生产环境

环境配置

// src/config/index.ts
export const config = {
apiUrl: process.env.REACT_APP_API_URL || 'https://api.example.com',
streamUrl: process.env.REACT_APP_STREAM_URL || 'https://stream.example.com',
timeout: Number.parseInt(process.env.REACT_APP_TIMEOUT || '10000'),
retryAttempts: Number.parseInt(process.env.REACT_APP_RETRY_ATTEMPTS || '3'),
debug: process.env.NODE_ENV === 'development'
};

生产环境优化

// src/api/production.ts
import { config } from '../config';

export const productionApi = hookFetch.create({
baseURL: config.apiUrl,
timeout: config.timeout,
plugins: [
// 生产环境插件
authPlugin(),
retryPlugin({ maxRetries: config.retryAttempts }),
cachePlugin({ ttl: 10 * 60 * 1000 }), // 10分钟缓存
// 注意:去重插件不推荐在生产环境使用,建议在应用层面处理
// dedupePlugin(), // 不推荐
errorHandlerPlugin(),
...(config.debug ? [loggerPlugin()] : []) // 只在调试模式下启用日志
]
});

通过遵循这些最佳实践,您可以构建出高性能、可维护且安全的应用程序。