关于是否应该二次封装 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 实例]
💎 结论:
- 避免为封装而封装:80% 的项目只需
axios.create()
即可
- 保持类型透明:优先使用 TypeScript 泛型而非运行时包装
- 敬畏原生能力:在扩展前先确认原生是否支持(如
axios.CancelToken
已被AbortController
替代)
如果现有封装层出现以下特征,请考虑重构:
- 无法直接访问原始请求对象
- 需要穿透封装层调用原生方法
- 错误处理需要 try-catch 嵌套超过两层
你们团队是否遇到过因过度封装导致的维护问题?欢迎分享具体场景探讨优化方案。