TypeScript HTTP 客户端:从 A 到 Z 正确发起请求
\n\n现代 Web 开发离不开与远程服务器的交互。无论是从 REST API 获取数据、提交表单还是上传文件,HTTP 请求都是客户端-服务器架构的基础。TypeScript 凭借其严格的类型化和强大的工具,不仅能让 HTTP 请求的处理更安全,还能使其更易于维护。在本文中,我们将探讨如何创建一个得体的 TypeScript HTTP 客户端,避免常见错误,并编写可预测运行的代码。
\n\n1. 基础:fetch API 及其类型化
\n\n浏览器内置的 fetch 是一个用于执行 HTTP 请求的现代且灵活的工具。然而,在 TypeScript 中使用它需要对响应进行正确的类型化。默认情况下,fetch 返回 Promise<Response>,我们需要明确指定期望接收的数据类型。
interface User {\n id: number;\n name: string;\n email: string;\n}\n\nasync function getUser(id: number): Promise<User> {\n const response = await fetch(`https://api.example.com/users/${id}`);\n \n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n \n const data: User = await response.json();\n return data;\n}\n\n\n请注意对 response.ok 的检查。这一点至关重要:即使服务器返回了错误(如 404、500 等),fetch 也不会自动抛出异常。我们必须手动处理状态码。
对于包含请求体的 POST 请求,需要指定 Content-Type 标头:
async function createUser(userData: Omit<User, 'id'>): Promise<User> {\n const response = await fetch('https://api.example.com/users', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(userData),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to create user: ${response.statusText}`);\n }\n\n return response.json();\n}\n\n\n2. 创建通用 HTTP 客户端
\n\n与其每次都从头编写 fetch,不如创建一个封装——一个通用的 HTTP 客户端。这样可以集中管理基础 URL、标头和错误处理。我们来看一个基于类的示例:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n\ninterface RequestConfig<TBody = unknown> {\n method: HttpMethod;\n url: string;\n body?: TBody;\n headers?: Record<string, string>;\n params?: Record<string, string>;\n}\n\nclass HttpClient {\n private baseUrl: string;\n private defaultHeaders: Record<string, string>;\n\n constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {\n this.baseUrl = baseUrl;\n this.defaultHeaders = {\n 'Content-Type': 'application/json',\n ...defaultHeaders,\n };\n }\n\n private async request<TResponse, TBody = unknown>(\n config: RequestConfig<TBody>\n ): Promise<TResponse> {\n const url = new URL(`${this.baseUrl}${config.url}`);\n \n if (config.params) {\n Object.entries(config.params).forEach(([key, value]) => {\n url.searchParams.append(key, value);\n });\n }\n\n const response = await fetch(url.toString(), {\n method: config.method,\n headers: { ...this.defaultHeaders, ...config.headers },\n body: config.body ? JSON.stringify(config.body) : undefined,\n });\n\n if (!response.ok) {\n const errorBody = await response.text();\n throw new Error(`Request failed: ${response.status} ${response.statusText}. Body: ${errorBody}`);\n }\n\n // 对于 DELETE 或其他可能不返回响应体的方法\n if (response.status === 204) {\n return undefined as TResponse;\n }\n\n return response.jso