Files
novalon-website/src/lib/api/client.ts
T
张翔 ac2672729f feat: 实现内容管理API及相关功能
refactor(services-section): 重构服务展示组件使用API数据
refactor(news-section): 重构新闻展示组件使用API数据
refactor(products-section): 重构产品展示组件使用API数据

test: 添加API客户端和服务钩子的单元测试
test(e2e): 添加配置验证和API响应格式的端到端测试

ci: 更新Playwright测试配置
2026-03-13 18:55:25 +08:00

222 lines
5.1 KiB
TypeScript

import { ApiResponse, RequestConfig } from './types';
class ApiError extends Error {
status: number;
code?: string;
constructor(status: number, message: string) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = status.toString();
}
}
class ApiClient {
private baseUrl: string = '';
private defaultTimeout: number = 5000;
private defaultRetries: number = 2;
constructor(baseUrl?: string) {
if (typeof window !== 'undefined') {
this.baseUrl = window.location.origin;
}
if (baseUrl) {
this.baseUrl = baseUrl;
}
}
async get<T = any>(
endpoint: string,
params?: Record<string, any>,
config?: RequestConfig
): Promise<T> {
const url = this.buildUrl(endpoint, params);
return this.request<T>(url, {
method: 'GET',
...config,
});
}
async post<T = any>(
endpoint: string,
data?: any,
config?: RequestConfig
): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
...config,
});
}
async put<T = any>(
endpoint: string,
data?: any,
config?: RequestConfig
): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
...config,
});
}
async delete<T = any>(
endpoint: string,
config?: RequestConfig
): Promise<T> {
return this.request<T>(endpoint, {
method: 'DELETE',
...config,
});
}
private async request<T>(
url: string,
options: RequestInit & RequestConfig = {}
): Promise<T> {
const {
retries = this.defaultRetries,
timeout = this.defaultTimeout,
headers = {},
...fetchOptions
} = options;
return this.executeWithRetry(
async () => {
const response = await this.fetchWithTimeout(url, {
...fetchOptions,
headers: {
'Content-Type': 'application/json',
...headers,
},
}, timeout);
if (!response) {
throw new Error('No response received');
}
if (!response.ok) {
const errorData = await this.parseError(response);
const error = this.createError(response.status, errorData.error || response.statusText);
throw error;
}
const data: ApiResponse<T> = await response.json();
if (!data.success) {
const error = this.createError(response.status, data.error || 'Request failed');
throw error;
}
return data.data;
},
retries,
(error) => {
const status = error?.status;
if (typeof status === 'number') {
return status >= 500 || status === 0;
}
return true;
}
);
}
private async executeWithRetry<T>(
fn: () => Promise<T>,
retries: number,
shouldRetry: (error: any) => boolean
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === retries || !shouldRetry(error)) {
throw error;
}
await this.delay(Math.pow(2, attempt) * 1000);
}
}
throw lastError;
}
private async fetchWithTimeout(
url: string,
options: RequestInit,
timeout: number
): Promise<Response> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response) {
throw new Error('No response received');
}
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === 'AbortError') {
const timeoutError = new Error('Request timeout') as any;
timeoutError.status = 0;
throw timeoutError;
}
throw error;
}
}
private async parseError(response: Response): Promise<{ error: string }> {
try {
const data = await response.json();
return data;
} catch {
return { error: response.statusText };
}
}
private createError(status: number, message: string): ApiError {
return new ApiError(status, message);
}
private buildUrl(endpoint: string, params?: Record<string, any>): string {
let url = endpoint;
if (this.baseUrl && !url.startsWith('http')) {
url = `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
}
if (!params || Object.keys(params).length === 0) {
return url;
}
const queryString = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryString.append(key, String(value));
}
});
return `${url}?${queryString.toString()}`;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
export const apiClient = new ApiClient();
export { ApiClient, ApiError };