跳到主要内容

插件系统

Hook-Fetch 的插件系统是其最强大的特性之一,允许您在请求的生命周期中插入自定义逻辑,实现高度可定制的功能。

插件概述

插件是一个对象,包含了在请求生命周期不同阶段执行的钩子函数。插件可以:

  • 修改请求配置
  • 处理响应数据
  • 转换流式数据
  • 处理错误
  • 执行清理操作

插件结构

interface HookFetchPlugin<T = unknown, E = unknown, P = unknown, D = unknown> {
name: string; // 插件名称(必需)
priority?: number; // 优先级(可选,默认为 0)
beforeRequest?: BeforeRequestHandler<E, P, D>;
afterResponse?: AfterResponseHandler<T, E, P, D>;
beforeStream?: BeforeStreamHandler<E, P, D>;
transformStreamChunk?: TransformStreamChunkHandler<E, P, D>;
onError?: OnErrorHandler<E, P, D>;
onFinally?: OnFinallyHandler<E, P, D>;
}

插件生命周期

插件的执行顺序如下:

  1. beforeRequest - 请求发送前
  2. beforeStream - 流式处理前(仅流式请求)
  3. transformStreamChunk - 流式数据块转换(仅流式请求)
  4. afterResponse - 响应接收后
  5. onError - 错误处理
  6. onFinally - 最终清理

使用插件

注册插件

// 创建实例时注册
const api = hookFetch.create({
baseURL: 'https://api.example.com',
plugins: [myPlugin(), anotherPlugin()]
});

// 或者使用 use 方法注册
api.use(myPlugin());

插件优先级

插件按优先级执行,数字越小优先级越高:

const highPriorityPlugin = {
name: 'high-priority',
priority: 1,
beforeRequest(config) {
// 优先执行
return config;
}
};

const lowPriorityPlugin = {
name: 'low-priority',
priority: 10,
beforeRequest(config) {
// 后执行
return config;
}
};

内置插件

SSE 文本解码插件

Hook-Fetch 提供了一个内置的 SSE(Server-Sent Events)文本解码插件:

import { sseTextDecoderPlugin } from 'hook-fetch/plugins/sse';

const api = hookFetch.create({
plugins: [
sseTextDecoderPlugin({
json: true, // 自动解析 JSON
prefix: 'data: ', // 移除前缀
splitSeparator: '\n\n', // 事件分隔符
doneSymbol: '[DONE]' // 结束标记
})
]
});

// 使用 SSE
for await (const chunk of api.get('/sse-endpoint').stream()) {
console.log(chunk.result); // 自动解析的数据
}

自定义插件示例

1. 认证插件

自动添加认证头:

const authPlugin = (getToken: () => string) => ({
name: 'auth',
priority: 1,
async beforeRequest(config) {
const token = getToken();
if (token) {
config.headers = new Headers(config.headers);
config.headers.set('Authorization', `Bearer ${token}`);
}
return config;
}
});

// 使用
const api = hookFetch.create({
plugins: [authPlugin(() => localStorage.getItem('token') || '')]
});

2. 日志插件

记录请求和响应:

const loggerPlugin = () => ({
name: 'logger',
async beforeRequest(config) {
console.log(`[${config.method}] ${config.url}`);
return config;
},
async afterResponse(context, config) {
console.log(`[${config.method}] ${config.url} - ${context.response.status}`);
return context;
},
async onError(error, config) {
console.error(`[${config.method}] ${config.url} - Error:`, error.message);
return error;
}
});

3. 重试插件

自动重试失败的请求:

const retryPlugin = (maxRetries = 3, delay = 1000) => ({
name: 'retry',
async onError(error, config) {
const retryCount = config.extra?.retryCount || 0;

if (retryCount < maxRetries && error.response?.status >= 500) {
// 延迟后重试
await new Promise(resolve => setTimeout(resolve, delay));

// 增加重试计数
config.extra = { ...config.extra, retryCount: retryCount + 1 };

// 重新发起请求
const newRequest = hookFetch(config.url, config);
return newRequest;
}

return error;
}
});

4. 缓存插件

缓存 GET 请求的响应:

const cachePlugin = (ttl = 5 * 60 * 1000) => {
const cache = new Map();

return {
name: 'cache',
async beforeRequest(config) {
if (config.method === 'GET') {
const key = `${config.url}?${new URLSearchParams(config.params).toString()}`;
const cached = cache.get(key);

if (cached && Date.now() - cached.timestamp < ttl) {
// 返回缓存的响应
throw new Response(JSON.stringify(cached.data), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
}
return config;
},
async afterResponse(context, config) {
if (config.method === 'GET' && context.response.ok) {
const key = `${config.url}?${new URLSearchParams(config.params).toString()}`;
cache.set(key, {
data: context.result,
timestamp: Date.now()
});
}
return context;
}
};
};

5. 流式数据转换插件

转换流式数据:

const streamTransformPlugin = () => ({
name: 'stream-transform',
async transformStreamChunk(chunk, config) {
if (!chunk.error && typeof chunk.result === 'string') {
try {
// 尝试解析 JSON
chunk.result = JSON.parse(chunk.result);
} catch {
// 如果不是 JSON,保持原样
}
}
return chunk;
}
});

6. 错误转换插件

统一错误格式:

const errorTransformPlugin = () => ({
name: 'error-transform',
async onError(error, config) {
if (error.response) {
const errorData = await error.response.json().catch(() => ({}));

// 创建统一的错误对象
const customError = new Error(errorData.message || 'Request failed');
customError.code = errorData.code || error.response.status;
customError.details = errorData.details;

return customError;
}
return error;
}
});

插件开发最佳实践

1. 命名规范

  • 使用描述性的名称
  • 避免与其他插件冲突
  • 使用 kebab-case 格式

2. 错误处理

const safePlugin = () => ({
name: 'safe-plugin',
async beforeRequest(config) {
try {
// 插件逻辑
return config;
} catch (error) {
console.error('Plugin error:', error);
return config; // 返回原始配置
}
}
});

3. 性能考虑

  • 避免在插件中执行耗时操作
  • 使用异步操作时要谨慎
  • 考虑缓存计算结果

4. 配置验证

const configurablePlugin = (options = {}) => {
const defaultOptions = {
enabled: true,
timeout: 5000
};

const config = { ...defaultOptions, ...options };

return {
name: 'configurable-plugin',
async beforeRequest(requestConfig) {
if (!config.enabled) return requestConfig;

// 插件逻辑
return requestConfig;
}
};
};

插件组合

可以组合多个插件来实现复杂功能:

const api = hookFetch.create({
plugins: [
authPlugin(() => getAuthToken()),
retryPlugin(3, 1000),
loggerPlugin(),
cachePlugin(10 * 60 * 1000), // 10分钟缓存
errorTransformPlugin()
]
});

调试插件

插件执行顺序

const debugPlugin = () => ({
name: 'debug',
priority: -1, // 最低优先级,最后执行
async beforeRequest(config) {
console.log('Plugin execution order - beforeRequest');
return config;
},
async afterResponse(context, config) {
console.log('Plugin execution order - afterResponse');
return context;
}
});

插件状态检查

// 检查已注册的插件
const api = hookFetch.create({
plugins: [plugin1(), plugin2()]
});

// 插件会按优先级排序并存储在实例中

高级插件模式

插件工厂

const createApiPlugin = (apiKey: string, baseURL: string) => ({
name: 'api-plugin',
async beforeRequest(config) {
config.headers = new Headers(config.headers);
config.headers.set('X-API-Key', apiKey);

if (!config.url.startsWith('http')) {
config.url = `${baseURL}${config.url}`;
}

return config;
}
});

// 使用
const api = hookFetch.create({
plugins: [createApiPlugin('my-api-key', 'https://api.example.com')]
});

条件插件

const conditionalPlugin = (condition: () => boolean) => ({
name: 'conditional',
async beforeRequest(config) {
if (condition()) {
// 只在满足条件时执行
config.headers = new Headers(config.headers);
config.headers.set('X-Conditional', 'true');
}
return config;
}
});

插件系统为 Hook-Fetch 提供了无限的扩展可能性,让您能够根据具体需求定制请求行为。