跳到主要内容

HTTP 请求封装

请求封装的重点,从来都不是“把 GET、POST 写成几个函数”。真正值得做的,是把业务里反复出现的共性问题收掉。

为什么项目里需要请求层

页面里如果直接写请求,代码通常很快会散掉:

  • URL 到处都是
  • token 逻辑到处都是
  • 错误处理每页各写一套
  • loading、重试、提示、权限失效逻辑越来越乱

所以项目里更稳的写法,一般会把请求层拆出来。

一个比较常见的分层

src/
api/
user.ts
order.ts
request/
client.ts
error.ts
auth.ts
types/

大致可以这么分:

  • client.ts:底层实例
  • api/*.ts:业务接口
  • error.ts:错误归一化
  • auth.ts:token 和鉴权边界

底层实例该做什么

不管用 fetch 还是 axios,底层实例一般会做这些:

  • baseURL
  • timeout
  • 默认 headers
  • token 注入
  • 统一错误处理
  • 业务码处理
  • 请求取消或重试基础能力

一个常见的 Axios 版本

import axios from 'axios'

export const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})

http.interceptors.request.use((config) => {
const token = localStorage.getItem('token')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
})

http.interceptors.response.use(
(response) => {
const res = response.data

if (res.code !== 0) {
throw new Error(res.message || 'request failed')
}

return res.data
},
(error) => {
return Promise.reject(error)
}
)

业务接口层怎么写

import { http } from '@/request/client'

export interface UserDetail {
id: string
name: string
}

export function getUserDetail(id: string) {
return http.get<UserDetail>(`/users/${id}`)
}

export function updateUserDetail(id: string, payload: Partial<UserDetail>) {
return http.put(`/users/${id}`, payload)
}

这样页面里拿到的是“用户接口函数”,而不是一堆底层请求细节。

常见封装点

1. 统一鉴权

  • token 放哪里取
  • 过期了怎么处理
  • 刷新 token 要不要重放请求

2. 统一错误

至少要区分 3 层:

  • 网络错误
  • HTTP 状态错误
  • 业务码错误

3. 超时和取消

搜索框、详情切换、组件卸载时,取消请求会很常见。

4. 文件上传下载

这类接口和普通 JSON 接口通常不该混着写。

5. 类型收口

接口层和业务层的类型要尽量稳定,不要让页面直接和后端裸结构硬绑死。

项目里最常见的误区

1. 封装得太薄

只是把 get/post 包一层,其实没有解决什么问题。

2. 封装得太厚

把一切都塞进一个超级请求函数里,后面调试会很痛苦。

3. 页面直接拼 URL 和业务码

只要页面里开始自己判断 res.code === 0,请求层边界就已经漏了。

和 TanStack Query 的关系

请求层和 Query 层不是替代关系。

  • 请求层解决“怎么发请求、怎么统一处理”
  • Query 层解决“数据怎么缓存、什么时候失效、怎么同步”

一个稳的项目,通常两层都会有。