Nuxt的HTTP客戶端封裝設計
σ
發(fā)布于 云南 2025-02-28 · 2782瀏覽

為什么要封裝HTTP客戶端

 

  Nuxt自帶$fetch和useFetch,為啥還要封裝?因為:

  1. 需要在客戶端和服務端共享代碼

  2. 需要統(tǒng)一處理認證、重試、日志

  3. useFetch只能在客戶端用,服務端API路由里不能用

 

  核心設計:工廠函數(shù)

 

  在app/utils/httpClient.ts里,用工廠函數(shù)創(chuàng)建HTTP客戶端:

 

  export const createHttpClient = (defaults: HttpClientOptions = {}): HttpClient => {

    const request = async <T>(url: string, options: HttpRequestOptions = {}): Promise<T> => {

      // 1. 合并headers(支持函數(shù)形式)

      const mergedHeaders = {

        ...resolveHeaders(defaults.headers),

        ...headers

      }

 

      // 2. 拼接完整URL

      const fullUrl = resolveUrl(baseURL ?? defaults.baseURL, url)

 

      // 3. 生命周期鉤子

      defaults.onRequest?.(fullUrl, fetchOptions)

 

      try {

        const response = await $fetch<T>(fullUrl, fetchOptions)

        defaults.onResponse?.(response)

        return response

      } catch (error) {

        defaults.onError?.(error)

        throw error

      }

    }

 

    // 返回便捷方法

    return {

      request,

      get: (url, options) => request(url, { ...options, method: 'GET' }),

      post: (url, body, options) => request(url, { ...options, method: 'POST', body }),

      // ...

    }

  }

 

  設計用于:

  - 支持動態(tài)headers(函數(shù)形式),可以在請求時才獲取token

  - 提供生命周期鉤子,方便打日志、做監(jiān)控

  - 封裝了常用的HTTP方法,調(diào)用更簡潔

 

  客戶端:全局$api實例

 

  在app/plugins/api.ts里注冊全局插件:

 

  export default defineNuxtPlugin(() => {

    const api = createHttpClient({

      baseURL: config.public.baseURL,

      headers: () => {

        // 動態(tài)獲取token,每次請求時才讀取cookie

        const token = useCookie('token').value

        return token ? { Authorization: `Bearer ${token}` } : {}

      },

      timeout: 15_000,

      retry: 1,

      onError: (error) => console.error('[client] api error', error)

    })

 

    return { provide: { api } }

  })

 

  在組件里用:

  const { $api } = useNuxtApp()

  const data = await $api.get('/api/users')

 

  為什么headers用函數(shù)?

 

  如果直接寫headers: { Authorization: useCookie('token').value },token只會在插件初始化時讀取一次。用戶登錄后token變了,但headers還是舊的。

 

  用函數(shù)形式headers: () => { ... },每次請求都會重新執(zhí)行,拿到最新的token。

 

  服務端:外部API客戶端

 

  在server/utils/apiClient.ts里創(chuàng)建服務端專用的客戶端:

 

  const getApiClients = () => {

    const config = useRuntimeConfig()

 

    const chatApi = createHttpClient({

      baseURL: config.chat.baseUrl,

      headers: {

        Authorization: `Bearer ${config.chat.apiKey}`,

        'Content-Type': 'application/json'

      },

      timeout: 30_000,

      retry: 3,

      onRequest: (url) => console.log(`請求外部API: ${url}`),

      onError: (error) => console.error('外部API錯誤:', error)

    })

 

    return { chatApi }

  }

 

  export const apiClients = getApiClients()

 

  在服務端API路由里用:

  import { apiClients } from '~/server/utils/apiClient'

 

  export default defineEventHandler(async (event) => {

    const data = await apiClients.chatApi.post('/completions', { prompt: '...' })

    return data

  })

 

  服務端和客戶端的區(qū)別:

  - 服務端用useRuntimeConfig()讀取環(huán)境變量(API密鑰等敏感信息)

  - 客戶端用useCookie()讀取用戶token

  - 兩者都用同一個createHttpClient工廠函數(shù),代碼復用

 

  類型安全的取舍

 

  有些時候代碼會用到as any:

  const response = await $fetch<T>(fullUrl, fetchOptions as any)

 

  這不是偷懶,是因為ofetch和Nuxt的$fetch泛型約束不一樣,強行滿足類型會很復雜。但運行時是安全的,因為控制了所有參數(shù)。

 

  類型安全很重要,但不要為了類型而類型。如果類型體操太復雜,影響可讀性,適當用as any也可以,前提是確定運行時安全。

σ
瀏覽 2782
相關推薦
最新評論
贊過的人
評論加載中...

暫無評論,快來評論吧!