blob: 623ee78666c77b4e9d950ea08aeeda6d57ab0228 [file] [log] [blame]
22301014bc4616f2025-06-03 16:59:44 +08001type RequestOptions = {
2 method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
3 headers?: Record<string, string>;
4 body?: unknown;
5 queryParams?: Record<string, string | number>;
6};
7
8type Interceptor = {
9 request?: (config: RequestInit) => RequestInit;
10 requestError?: (error: unknown) => Promise<never>; // 错误拦截器应始终抛出
11 response?: (response: Response) => Response | Promise<Response>;
12 // 错误拦截器必须抛出或返回与请求相同类型的结果
13 responseError?: <T>(error: unknown) => Promise<T>;
14};
15
16export class HttpClient {
17 private static instance: HttpClient;
18 private baseUrl: string;
19 private interceptors: Interceptor[] = [];
20
21 private constructor(baseUrl: string) {
22 this.baseUrl = baseUrl;
23 }
24
25 public static getInstance(baseUrl: string): HttpClient {
26 if (!HttpClient.instance) {
27 HttpClient.instance = new HttpClient(baseUrl);
28 }
29 return HttpClient.instance;
30 }
31
32 // 添加拦截器
33 addInterceptor(interceptor: Interceptor) {
34 this.interceptors.push(interceptor);
35 return this; // 支持链式调用
36 }
37
38 async request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
39 const {
40 method = 'GET',
41 headers = {},
42 body,
43 queryParams = {}
44 } = options;
45
46 const url = new URL(`${this.baseUrl}${endpoint}`);
47 Object.entries(queryParams).forEach(([key, value]) => {
48 url.searchParams.append(key, String(value));
49 });
50
51 let config: RequestInit = {
52 method,
53 headers: {
54 'Content-Type': 'application/json',
55 ...headers
56 }
57 };
58
59 if (body) {
60 config.body = JSON.stringify(body);
61 }
62
63 // 请求拦截器处理
64 for (const interceptor of this.interceptors) {
65 if (interceptor.request) {
66 config = interceptor.request(config);
67 }
68 }
69
70 try {
71 let response = await fetch(url.toString(), config);
72
73 // 响应拦截器处理
74 for (const interceptor of this.interceptors) {
75 if (interceptor.response) {
76 response = await interceptor.response(response);
77 }
78 }
79
80 if (!response.ok) {
81 const errorData = await response.json();
82 throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorData.message || 'Unknown error'}`);
83 }
84
85 return await response.json();
86 } catch (error) {
87 // 改进的错误拦截器处理
88 let lastError = error;
89
90 for (const interceptor of this.interceptors) {
91 if (interceptor.responseError) {
92 try {
93 // 错误拦截器必须返回Promise<T>或抛出
94 return await interceptor.responseError<T>(lastError);
95 } catch (e) {
96 // 保存最新错误并继续下一个拦截器
97 lastError = e;
98 console.error('Interceptor error:', e);
99 }
100 }
101 }
102
103 // 如果所有拦截器都未能处理错误,则抛出最后一个错误
104 throw lastError;
105 }
106 }
107 get<T>(endpoint: string, queryParams?: Record<string, string | number>) {
108 return this.request<T>(endpoint, { queryParams });
109 }
110
111 post<T>(endpoint: string, body?: unknown, headers?: Record<string, string>) {
112 return this.request<T>(endpoint, { method: 'POST', body, headers });
113 }
114
115 put<T>(endpoint: string, body?: unknown, headers?: Record<string, string>) {
116 return this.request<T>(endpoint, { method: 'PUT', body, headers });
117 }
118
119 delete<T>(endpoint: string, headers?: Record<string, string>) {
120 return this.request<T>(endpoint, { method: 'DELETE', headers });
121 }
122}
123
124const client = HttpClient.getInstance('http://localhost:8080/');
125// 添加Token注入拦截器
126client.addInterceptor({
127 request: (config) => {
128 const token = localStorage.getItem('token'); // 从存储中获取token
129 if (token) {
130 config.headers = {
131 ...config.headers,
132 'Authorization': `Bearer ${token}`
133 };
134 }
135 return config;
136 },
137 responseError: async (error) => {
138 if (error instanceof Error && error.message.includes('401')) {
139 console.log('Token expired, redirecting to login...');
140 // 示例:重定向到登录页
141 window.location.href = '/login';
142 // 必须返回Promise<never>或抛出
143 return new Promise(() => {}); // 永远不解决的Promise
144 }
145 throw error;
146 }
147});
148export default client as HttpClient;