關(guān)于是否應(yīng)該二次封裝 axios 的問題,我的觀點是:謹慎封裝,避免過度設(shè)計。二次封裝并非“靈丹妙藥”,盲目封裝反而可能引入新的問題。以下是深度分析:
?? 為什么二次封裝 axios 容易成為“反模式”?
1. 過度抽象,破壞原生能力
// 典型反例:將 axios 完全隱藏
const myHttp = {
get: (url) => axios.get(url).then(res => res.data.data) // 強制截取 data.data
}
- 問題:
- 強行統(tǒng)一響應(yīng)結(jié)構(gòu),導(dǎo)致無法訪問原生響應(yīng)頭、狀態(tài)碼等元信息
- 攔截了非常規(guī)響應(yīng)(如 302 重定向、流數(shù)據(jù))
- 強行統(tǒng)一響應(yīng)結(jié)構(gòu),導(dǎo)致無法訪問原生響應(yīng)頭、狀態(tài)碼等元信息
2. 冗余分層,增加維護成本
// 多層封裝導(dǎo)致調(diào)用鏈路變深
import http from '@libs/http' // 第一層:基礎(chǔ)封裝
import api from '@services/api' // 第二層:業(yè)務(wù)封裝
api.user.getList() // 實際調(diào)用鏈:api -> http -> axios
- 問題:
- 調(diào)試時需跳轉(zhuǎn)多級文件
- 底層 axios 升級時需穿透多層適配
- 調(diào)試時需跳轉(zhuǎn)多級文件
3. 錯誤處理陷入“俄羅斯套娃”
// 每層都做錯誤處理導(dǎo)致邏輯混亂
// 封裝層
try {
await axios.get(url)
} catch (err) {
showToast('網(wǎng)絡(luò)錯誤') // 通用提示可能破壞業(yè)務(wù)邏輯
}
// 業(yè)務(wù)層
try {
await api.getData()
} catch (err) {
// 此處可能永遠捕獲不到錯誤
}
? 合理封裝的 3 個原則
1. 最小化封裝(Less is More)
// 僅做必要擴展,保留原生API
const http = axios.create({
timeout: 10000,
headers: { 'X-Env': 'web' }
})
// 保留直接使用 axios 的能力
export { http, axios as originalAxios }
2. 非侵入式攔截器
// 請求攔截:只添加元信息,不修改數(shù)據(jù)
http.interceptors.request.use(config => {
config.headers.Auth = getToken()
return config // 保持結(jié)構(gòu)透明
})
// 響應(yīng)攔截:區(qū)分業(yè)務(wù)錯誤和系統(tǒng)錯誤
http.interceptors.response.use(
response => response, // 直接返回原始響應(yīng)
error => {
if (error.response?.status === 401) {
redirectToLogin()
}
return Promise.reject(error) // 繼續(xù)拋出原始錯誤
}
)
3. 提供 TypeScript 增強而非覆蓋
// 擴展業(yè)務(wù)響應(yīng)類型,而非重寫 axios 類型
interface ApiResponse<T> {
code: number
data: T
msg?: string
}
// 業(yè)務(wù)方法明確類型約束
export function getUser<T>(id: string) {
return http.get<ApiResponse<T>>(`/user/${id}`)
}
?? 更現(xiàn)代的替代方案
1. **直接使用 axios.create()
創(chuàng)建實例
// 滿足 90% 場景的最佳實踐
export const http = axios.create({
baseURL: import.meta.env.VITE_API_URL,
validateStatus: status => status < 500 // 不自動 reject 4xx 狀態(tài)碼
})
2. **使用 fetch
+ 類型助手(原生方案)
// 簡單封裝示例
export async function safeFetch<T>(input: RequestInfo, init?: RequestInit) {
const res = await fetch(input, init)
if (!res.ok) throw new Error(`${res.status}`)
return res.json() as Promise<T> // 顯式類型聲明
}
// 使用
const data = await safeFetch<{ id: string }>('/api/user')
3. **選擇專為TS設(shè)計的請求庫(如 ofetch
/ ky
)
// ofetch 示例:自帶類型推導(dǎo)
import { ofetch } from 'ofetch'
const data = await ofetch<User>('/api/user', {
query: { page: 1 },
parseResponse: JSON.parse
})
?? 決策流程圖:是否需要封裝?
graph TD A[新項目啟動] --> B{需要統(tǒng)一處理?<br/> 如:鑒權(quán)/錯誤碼/埋點} B -->|是| C[用 axios.create 創(chuàng)建實例<br/>+ 輕量攔截器] B -->|否| D[直接裸用 axios/fetch] C --> E{需要業(yè)務(wù)語義化?} E -->|是| F[按模塊定義業(yè)務(wù)函數(shù)<br/>export function getUser] E -->|否| G[直接導(dǎo)出 http 實例]
?? 結(jié)論:
- 避免為封裝而封裝:80% 的項目只需
axios.create()
即可
- 保持類型透明:優(yōu)先使用 TypeScript 泛型而非運行時包裝
- 敬畏原生能力:在擴展前先確認原生是否支持(如
axios.CancelToken
已被AbortController
替代)
如果現(xiàn)有封裝層出現(xiàn)以下特征,請考慮重構(gòu):
- 無法直接訪問原始請求對象
- 需要穿透封裝層調(diào)用原生方法
- 錯誤處理需要 try-catch 嵌套超過兩層
你們團隊是否遇到過因過度封裝導(dǎo)致的維護問題?歡迎分享具體場景探討優(yōu)化方案。