為什么要封裝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也可以,前提是確定運行時安全。





暫無評論,快來評論吧!