不要再二次封装 axios 了,它真的是“灵丹妙药”吗?

关于是否应该二次封装 axios 的问题,我的观点是:谨慎封装,避免过度设计。二次封装并非“灵丹妙药”,盲目封装反而可能引入新的问题。以下是深度分析:


⚠️ 为什么二次封装 axios 容易成为“反模式”?

1. 过度抽象,破坏原生能力

   // 典型反例:将 axios 完全隐藏
   const myHttp = {
     get: (url) => axios.get(url).then(res => res.data.data) // 强制截取 data.data
   }
  • 问题
    • 强行统一响应结构,导致无法访问原生响应头、状态码等元信息
    • 拦截了非常规响应(如 302 重定向、流数据)

2. 冗余分层,增加维护成本

   // 多层封装导致调用链路变深
   import http from '@libs/http'   // 第一层:基础封装
   import api from '@services/api' // 第二层:业务封装

   api.user.getList() // 实际调用链:api -> http -> axios
  • 问题
    • 调试时需跳转多级文件
    • 底层 axios 升级时需穿透多层适配

3. 错误处理陷入“俄罗斯套娃”

   // 每层都做错误处理导致逻辑混乱
   // 封装层
   try {
     await axios.get(url)
   } catch (err) {
     showToast('网络错误') // 通用提示可能破坏业务逻辑
   }

   // 业务层
   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. 非侵入式拦截器

   // 请求拦截:只添加元信息,不修改数据
   http.interceptors.request.use(config => {
     config.headers.Auth = getToken()
     return config // 保持结构透明
   })

   // 响应拦截:区分业务错误和系统错误
   http.interceptors.response.use(
     response => response, // 直接返回原始响应
     error => {
       if (error.response?.status === 401) {
         redirectToLogin()
       }
       return Promise.reject(error) // 继续抛出原始错误
     }
   )

3. 提供 TypeScript 增强而非覆盖

   // 扩展业务响应类型,而非重写 axios 类型
   interface ApiResponse<T> {
     code: number
     data: T
     msg?: string
   }

   // 业务方法明确类型约束
   export function getUser<T>(id: string) {
     return http.get<ApiResponse<T>>(`/user/${id}`)
   }

🔥 更现代的替代方案

1. **直接使用 axios.create() 创建实例

   // 满足 90% 场景的最佳实践
   export const http = axios.create({
     baseURL: import.meta.env.VITE_API_URL,
     validateStatus: status => status < 500 // 不自动 reject 4xx 状态码
   })

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设计的请求库(如 ofetch / ky

   // ofetch 示例:自带类型推导
   import { ofetch } from 'ofetch'

   const data = await ofetch<User>('/api/user', {
     query: { page: 1 },
     parseResponse: JSON.parse
   })

📊 决策流程图:是否需要封装?

graph TD
  A[新项目启动] --> B{需要统一处理?<br/> 如:鉴权/错误码/埋点}
  B -->|是| C[用 axios.create 创建实例<br/>+ 轻量拦截器]
  B -->|否| D[直接裸用 axios/fetch]
  C --> E{需要业务语义化?}
  E -->|是| F[按模块定义业务函数<br/>export function getUser]
  E -->|否| G[直接导出 http 实例]

💎 结论:

  1. 避免为封装而封装:80% 的项目只需 axios.create() 即可
  2. 保持类型透明:优先使用 TypeScript 泛型而非运行时包装
  3. 敬畏原生能力:在扩展前先确认原生是否支持(如 axios.CancelToken 已被 AbortController 替代)

如果现有封装层出现以下特征,请考虑重构:

  • 无法直接访问原始请求对象
  • 需要穿透封装层调用原生方法
  • 错误处理需要 try-catch 嵌套超过两层

你们团队是否遇到过因过度封装导致的维护问题?欢迎分享具体场景探讨优化方案。