引言:为什么正确的HTTP客户端是现代Web应用的基础
任何现代Web应用都通过HTTP与服务器交互。无论是加载商品列表、提交注册表单还是获取仪表盘数据——这一切都归结为发送HTTP请求。在TypeScript中,凭借其强大的类型系统,我们可以构建一个不仅可用,而且真正可靠且可预测的HTTP客户端。
在本文中,我们将探讨如何创建一个TypeScript HTTP客户端,该客户端能够处理错误、正确地对响应进行类型化、管理标头并支持中间件。您将了解到为什么使用原始的fetch而不进行封装是一种不良实践,以及如何将其转变为强大的工具。
基础:从原始fetch到类型化客户端
让我们从一个基本示例开始。JavaScript内置的fetch是一个很好的起点,但它不提供方便的类型化和错误处理。在TypeScript中,我们可以将其封装到自己的函数中。
简单的fetch封装
创建一个基础类HttpClient,它将接收基础URL和设置。
interface RequestOptions {
headers?: Record<string, string>;
params?: Record<string, string>;
}
class HttpClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
this.baseUrl = baseUrl;
this.defaultHeaders = defaultHeaders;
}
private buildUrl(path: string, params?: Record<string, string>): string {
const url = new URL(`${this.baseUrl}${path}`);
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value));
}
return url.toString();
}
async get<T>(path: string, options?: RequestOptions): Promise<T> {
const url = this.buildUrl(path, options?.params);
const response = await fetch(url, {
method: 'GET',
headers: { ...this.defaultHeaders, ...options?.headers },
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
}
现在调用client.get<User>('/users/1')将返回一个类型化的User对象。这比使用any要好得多。
高级错误处理和响应类型化
简单地抛出错误并不是最佳方法。我们需要区分网络错误、服务器错误(4xx、5xx)和JSON解析错误。让我们创建自己的错误类。
创建错误类
class HttpError extends Error {
status: number;
body: any;
constructor(message: string, status: number, body?: any) {
super(message);
this.name = 'HttpError';
this.status = status;
this.body = body;
}
}
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = 'NetworkError';
}
}
class ParseError extends Error {
constructor(message: string) {
super(message);
this.name = 'ParseError';
}
}
在get方法中改进错误处理
async get<T>(path: string, options?: RequestOptions): Promise<T> {
try {
const url = this.buildUrl(path, options?.params);
const response = await fetch(url, {
method: 'GET',
headers: { ...this.defaultHeaders, ...options?.headers },
});
let body: any;
try {
body = await response.json();
} catch {
throw new ParseError('Failed to parse response as JSON');
}
if (!response.ok) {
throw new HttpError(
`Request failed with status ${response.status}`,
response.status,
body
);
}
return body as T;
} catch (er