Quellcode durchsuchen

refactor(utils):重构 HTTP 客户端以使用原生 fetch API- 移除对 axios 的依赖,改用原生 fetch 实现
- 支持 Next.js 缓存选项(cache、revalidate、tags)
- 添加请求超时控制和 AbortSignal 支持
- 实现 GET、POST、PUT、DELETE 方法的统一错误处理
- 改进参数序列化逻辑,支持数组格式化选项- 更新错误类型定义,提供更准确的错误信息- 优化 URL 构建方法,支持查询参数序列化
- 调整类结构,移除拦截器相关逻辑- 更新响应数据解析方式,统一返回格式
- 改进类型定义,提升代码可维护性

nahida vor 7 Monaten
Ursprung
Commit
6d9d5998bf
2 geänderte Dateien mit 160 neuen und 112 gelöschten Zeilen
  1. 0 0
      src/app/news/[id]/rich.css
  2. 160 112
      src/utils/request.ts

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
src/app/news/[id]/rich.css


+ 160 - 112
src/utils/request.ts

@@ -1,11 +1,3 @@
-import axios, {
-  type AxiosError,
-  type AxiosInstance,
-  type AxiosRequestConfig,
-  type AxiosResponse,
-  type InternalAxiosRequestConfig,
-} from "axios"
-
 // 定义通用的 API 响应接口
 interface ApiResponse<T = any> {
   data: T
@@ -13,174 +5,230 @@ interface ApiResponse<T = any> {
   msg: string
 }
 
-// 扩展 Axios 请求配置
-interface RequestConfig extends AxiosRequestConfig {
+interface RequestConfig {
   timeout?: number
   headers?: Record<string, string>
+  signal?: AbortSignal
+  // Next.js 缓存选项
+  cache?: "force-cache" | "no-store" | "no-cache" | "default"
+  next?: {
+    revalidate?: number | false
+    tags?: string[]
+  }
 }
 
 // 定义可能的错误类型
-type HttpClientError = AxiosError | Error
+type HttpClientError = Error
 
 class ServerHttpClient {
-  private instance: AxiosInstance
+  private baseURL: string
+  private defaultTimeout: number
+  private defaultHeaders: Record<string, string>
 
   constructor() {
-    // 创建axios实例
-    this.instance = axios.create({
-      baseURL: process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080",
-      timeout: 10000,
-      headers: {
-        "Content-Type": "application/json",
-      },
-    })
-
-    this.setupRequestInterceptors()
-    this.setupResponseInterceptors()
-  }
-
-  private setupRequestInterceptors() {
-    this.instance.interceptors.request.use(
-      (config: InternalAxiosRequestConfig) => {
-        return config
-      },
-      (error: any) => {
-        return Promise.reject(error)
-      },
-    )
-  }
-
-  private setupResponseInterceptors() {
-    this.instance.interceptors.response.use(
-      (response: AxiosResponse<ApiResponse<any>>) => {
-        return response
-      },
-      (error: AxiosError) => {
-        // 服务端只返回错误,不做 UI 提示
-        return Promise.reject(error)
-      },
-    )
+    this.baseURL = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080"
+    this.defaultTimeout = 10000
+    this.defaultHeaders = {
+      "Content-Type": "application/json",
+    }
   }
 
-  // GET 请求
   async serverGet<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
     url: string,
     params?: TParams,
     config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
   ): Promise<ApiResponse<TResponse>> {
     try {
-      const response = await this.instance.get<ApiResponse<TResponse>>(url, {
-        params,
-        paramsSerializer: (p) => {
-          const parts: string[] = []
-          for (const key in p) {
-            const value = (p as any)[key]
-            if (Array.isArray(value)) {
-              switch (config?.arrayFormat ?? "indices") {
-                case "repeat":
-                  value.forEach((v) => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`))
-                  break
-                case "comma":
-                  parts.push(`${encodeURIComponent(key)}=${value.map((v) => encodeURIComponent(v)).join(",")}`)
-                  break
-                case "indices":
-                default:
-                  value.forEach((v, i) => parts.push(`${encodeURIComponent(key)}[${i}]=${encodeURIComponent(v)}`))
-                  break
-              }
-            } else if (value !== undefined && value !== null) {
-              parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
-            }
-          }
-          return parts.join("&")
+      const fullUrl = this.buildUrl(url, params, config?.arrayFormat)
+      const controller = new AbortController()
+      const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
+
+      const response = await fetch(fullUrl, {
+        method: "GET",
+        headers: {
+          ...this.defaultHeaders,
+          ...config?.headers,
         },
-        ...config,
+        signal: config?.signal ?? controller.signal,
+        cache: config?.cache,
+        next: config?.next,
       })
-      return response.data
+
+      clearTimeout(timeoutId)
+
+      if (!response.ok) {
+        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
+      }
+
+      const data: ApiResponse<TResponse> = await response.json()
+      return data
     } catch (error: any) {
       throw this.handleError(error)
     }
   }
 
-  // POST 请求
   async serverPost<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
     url: string,
     data?: TData,
     config?: RequestConfig,
   ): Promise<ApiResponse<TResponse>> {
     try {
-      const response = await this.instance.post<ApiResponse<TResponse>>(url, JSON.stringify(data), config)
-      return response.data
+      const fullUrl = `${this.baseURL}${url}`
+      const controller = new AbortController()
+      const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
+
+      const response = await fetch(fullUrl, {
+        method: "POST",
+        headers: {
+          ...this.defaultHeaders,
+          ...config?.headers,
+        },
+        body: data ? JSON.stringify(data) : undefined,
+        signal: config?.signal ?? controller.signal,
+        cache: config?.cache,
+        next: config?.next,
+      })
+
+      clearTimeout(timeoutId)
+
+      if (!response.ok) {
+        const errorData = await response.json().catch(() => ({}))
+        throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
+      }
+
+      const responseData: ApiResponse<TResponse> = await response.json()
+      return responseData
     } catch (error: any) {
       throw this.handleError(error)
     }
   }
 
-  // PUT 请求
   async serverPut<TResponse = any, TData extends Record<string, any> | null = Record<string, any> | null>(
     url: string,
     data?: TData,
     config?: RequestConfig,
   ): Promise<ApiResponse<TResponse>> {
     try {
-      const response = await this.instance.put<ApiResponse<TResponse>>(url, JSON.stringify(data), config)
-      return response.data
+      const fullUrl = `${this.baseURL}${url}`
+      const controller = new AbortController()
+      const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
+
+      const response = await fetch(fullUrl, {
+        method: "PUT",
+        headers: {
+          ...this.defaultHeaders,
+          ...config?.headers,
+        },
+        body: data ? JSON.stringify(data) : undefined,
+        signal: config?.signal ?? controller.signal,
+        cache: config?.cache,
+        next: config?.next,
+      })
+
+      clearTimeout(timeoutId)
+
+      if (!response.ok) {
+        const errorData = await response.json().catch(() => ({}))
+        throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
+      }
+
+      const responseData: ApiResponse<TResponse> = await response.json()
+      return responseData
     } catch (error: any) {
       throw this.handleError(error)
     }
   }
 
-  // DELETE 请求
   async serverDelete<TResponse = any, TParams extends Record<string, any> | null = Record<string, any> | null>(
     url: string,
     params?: TParams,
     config?: RequestConfig & { arrayFormat?: "indices" | "repeat" | "comma" },
   ): Promise<ApiResponse<TResponse>> {
     try {
-      const response = await this.instance.delete<ApiResponse<TResponse>>(url, {
-        params,
-        paramsSerializer: (p) => {
-          const parts: string[] = []
-          for (const key in p) {
-            const value = (p as any)[key]
-            if (Array.isArray(value)) {
-              switch (config?.arrayFormat ?? "indices") {
-                case "repeat":
-                  value.forEach((v) => parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`))
-                  break
-                case "comma":
-                  parts.push(`${encodeURIComponent(key)}=${value.map((v) => encodeURIComponent(v)).join(",")}`)
-                  break
-                case "indices":
-                default:
-                  value.forEach((v, i) => parts.push(`${encodeURIComponent(key)}[${i}]=${encodeURIComponent(v)}`))
-                  break
-              }
-            } else if (value !== undefined && value !== null) {
-              parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
-            }
-          }
-          return parts.join("&")
+      const fullUrl = this.buildUrl(url, params, config?.arrayFormat)
+      const controller = new AbortController()
+      const timeoutId = setTimeout(() => controller.abort(), config?.timeout ?? this.defaultTimeout)
+
+      const response = await fetch(fullUrl, {
+        method: "DELETE",
+        headers: {
+          ...this.defaultHeaders,
+          ...config?.headers,
         },
-        ...config,
+        signal: config?.signal ?? controller.signal,
+        cache: config?.cache,
+        next: config?.next,
       })
-      return response.data
+
+      clearTimeout(timeoutId)
+
+      if (!response.ok) {
+        const errorData = await response.json().catch(() => ({}))
+        throw new Error((errorData as any)?.msg || `HTTP ${response.status}: ${response.statusText}`)
+      }
+
+      const responseData: ApiResponse<TResponse> = await response.json()
+      return responseData
     } catch (error: any) {
       throw this.handleError(error)
     }
   }
 
-  // 错误处理
+  private buildUrl(
+    url: string,
+    params?: Record<string, any> | null,
+    arrayFormat: "indices" | "repeat" | "comma" = "indices",
+  ): string {
+    const fullUrl = `${this.baseURL}${url}`
+
+    if (!params) {
+      return fullUrl
+    }
+
+    const searchParams = new URLSearchParams()
+
+    for (const key in params) {
+      const value = params[key]
+      if (Array.isArray(value)) {
+        switch (arrayFormat) {
+          case "repeat":
+            value.forEach((v) => searchParams.append(key, String(v)))
+            break
+          case "comma":
+            searchParams.set(key, value.map((v) => String(v)).join(","))
+            break
+          case "indices":
+          default:
+            value.forEach((v, i) => searchParams.set(`${key}[${i}]`, String(v)))
+            break
+        }
+      } else if (value !== undefined && value !== null) {
+        searchParams.set(key, String(value))
+      }
+    }
+
+    const queryString = searchParams.toString()
+    return queryString ? `${fullUrl}?${queryString}` : fullUrl
+  }
+
   private handleError(error: any): HttpClientError {
-    if (axios.isAxiosError(error)) {
-      const message = (error.response?.data as any)?.msg || error.message || "请求失败"
-      return new Error(message)
+    if (error.name === "AbortError") {
+      return new Error("请求超时")
     }
+
+    if (error instanceof TypeError && error.message.includes("fetch")) {
+      return new Error("网络连接失败")
+    }
+
     return error instanceof Error ? error : new Error("未知错误")
   }
 
-  getInstance(): AxiosInstance {
-    return this.instance
+  getInstance() {
+    return {
+      baseURL: this.baseURL,
+      timeout: this.defaultTimeout,
+      headers: this.defaultHeaders,
+    }
   }
 }
 

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.