關(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ù))

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 升級時需穿透多層適配

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é)論:

  1. 避免為封裝而封裝:80% 的項目只需 axios.create() 即可
  2. 保持類型透明:優(yōu)先使用 TypeScript 泛型而非運行時包裝
  3. 敬畏原生能力:在擴展前先確認原生是否支持(如 axios.CancelToken 已被 AbortController 替代)

如果現(xiàn)有封裝層出現(xiàn)以下特征,請考慮重構(gòu):

  • 無法直接訪問原始請求對象
  • 需要穿透封裝層調(diào)用原生方法
  • 錯誤處理需要 try-catch 嵌套超過兩層

你們團隊是否遇到過因過度封裝導(dǎo)致的維護問題?歡迎分享具體場景探討優(yōu)化方案。