1. 为什么我们需要封装fetch?
作为一名前端开发者,我深知原生fetch API的痛点。每次看到项目中重复的.then().catch()链式调用,还有那些无处不在的状态码判断,我就忍不住想:这真的有必要吗?
1.1 原生fetch的三大痛点
首先,让我们直面原生fetch的不足之处:
-
冗余的错误处理:每个请求都需要手动检查
res.ok,处理HTTP状态码,还要捕获可能的JSON解析错误。我曾经在一个项目中统计过,光是错误处理的代码就占了整个请求逻辑的70%。 -
缺乏统一配置:想要给所有请求添加统一的headers?比如Authorization token?原生fetch下你只能在每个调用处重复添加,维护起来简直是噩梦。
-
功能缺失:超时控制?请求取消?重试机制?这些常见需求fetch都没有内置支持,需要开发者自己实现。
1.2 封装的价值所在
封装fetch的核心价值在于:
- 减少重复代码:把通用的逻辑(如错误处理、headers设置)集中管理
- 统一行为:确保所有请求都遵循相同的处理流程
- 增强功能:添加超时、重试、拦截器等高级特性
- 提高可维护性:配置和逻辑集中在一处,修改时只需改动一个地方
typescript复制// 原生fetch vs 封装后的对比
// 原生 - 每个请求都要写这么多
fetch('/api/user', {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
})
.then(res => {
if (!res.ok) throw new Error(res.statusText)
return res.json()
})
.then(data => console.log(data))
.catch(err => console.error(err))
// 封装后 - 简洁明了
api.get('/user').then(console.log).catch(console.error)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心封装方案详解
2.1 基础请求封装
让我们从最基础的封装开始。首先定义一个TypeScript接口来描述我们的请求配置:
typescript复制interface RequestConfig extends RequestInit {
url: string
baseURL?: string
timeout?: number
params?: Record<string, any>
data?: any
}
然后实现核心的request函数:
typescript复制async function request<T>(config: RequestConfig): Promise<T> {
const {
url,
baseURL = '',
timeout = 10000,
headers = {},
params,
data,
...rest
} = config
// 处理URL和查询参数
const fullUrl = new URL(url.startsWith('http') ? url : `${baseURL}${url}`)
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
fullUrl.searchParams.append(key, String(value))
}
})
}
// 处理请求体
let body: BodyInit | null = null
if (data) {
body = JSON.stringify(data)
headers['Content-Type'] = 'application/json'
}
// 超时控制
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(fullUrl.toString(), {
...rest,
headers,
body,
signal: controller.signal
})
clearTimeout(timeoutId)
// 处理响应
if (!response.ok) {
const errorData = await parseErrorResponse(response)
throw new HttpError(response.status, errorData)
}
return parseResponse<T>(response)
} catch (error) {
clearTimeout(timeoutId)
throw normalizeError(error)
}
}
2.2 响应和错误处理
良好的错误处理是封装的关键。我们需要统一处理各种可能的错误情况:
typescript复制// 自定义HTTP错误类
class HttpError extends Error {
constructor(
public status: number,
public data: any
) {
super(`HTTP Error: ${status}`)
}
}
// 解析响应
async function parseResponse<T>(response: Response): Promise<T> {
const contentType = response.headers.get('content-type')
if (contentType?.includes('application/json')) {
return response.json()
}
if (contentType?.includes('text/')) {
return response.text() as any
}
return response.blob() as any
}
// 解析错误响应
async function parseErrorResponse(response: Response) {
try {
return await response.json()
} catch {
return { message: response.statusText }
}
}
// 标准化错误
function n
