强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

18 - HTTP 请求

HTTP 请求

fetch API

基本用法

// fetch 返回 Promise<Response>
const response = await fetch("https://api.example.com/users");

// Response 对象
console.log(response.status);      // number
console.log(response.statusText);  // string
console.log(response.ok);          // boolean
console.log(response.headers);     // Headers

// 解析响应
const text = await response.text();   // string
const json = await response.json();   // any
const blob = await response.blob();   // Blob
const arrayBuffer = await response.arrayBuffer(); // ArrayBuffer

类型安全的 fetch

// 通用请求函数
async function request<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, options);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return response.json();
}

// GET 请求
interface User {
  id: number;
  name: string;
  email: string;
}

const users = await request<User[]>("/api/users");
// users: User[]

const user = await request<User>("/api/users/1");
// user: User

GET 请求

async function get<T>(url: string, params?: Record<string, string>): Promise<T> {
  const queryString = params
    ? "?" + new URLSearchParams(params).toString()
    : "";

  return request<T>(url + queryString);
}

// 使用
const users = await get<User[]>("/api/users", { page: "1", limit: "10" });

POST 请求

async function post<T, B = any>(url: string, body: B): Promise<T> {
  return request<T>(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  });
}

// 使用
interface CreateUserDto {
  name: string;
  email: string;
}

const newUser = await post<User, CreateUserDto>("/api/users", {
  name: "Alice",
  email: "[email protected]"
});

PUT / PATCH / DELETE

async function put<T, B = any>(url: string, body: B): Promise<T> {
  return request<T>(url, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  });
}

async function patch<T, B = any>(url: string, body: B): Promise<T> {
  return request<T>(url, {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  });
}

async function del<T = void>(url: string): Promise<T> {
  return request<T>(url, { method: "DELETE" });
}

// 使用
await put<User, Partial<User>>("/api/users/1", { name: "Bob" });
await patch<User, Partial<User>>("/api/users/1", { name: "Bob" });
await del("/api/users/1");

axios

安装

npm install axios
npm install -D @types/axios  # 通常不需要,axios 自带类型

基本用法

import axios, { AxiosResponse, AxiosError } from "axios";

// GET
const response: AxiosResponse<User[]> = await axios.get("/api/users");
console.log(response.data);   // User[]
console.log(response.status); // number
console.log(response.headers); // any

// POST
const created: AxiosResponse<User> = await axios.post("/api/users", {
  name: "Alice",
  email: "[email protected]"
});

创建实例

const api = axios.create({
  baseURL: "https://api.example.com",
  timeout: 10000,
  headers: {
    "Content-Type": "application/json"
  }
});

// 请求拦截器
api.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器
api.interceptors.response.use(
  (response) => response,
  (error: AxiosError) => {
    if (error.response?.status === 401) {
      // 跳转登录
    }
    return Promise.reject(error);
  }
);

泛型封装

class ApiClient {
  private client: typeof axios;

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      headers: { "Content-Type": "application/json" }
    });
  }

  async get<T>(url: string, params?: any): Promise<T> {
    const response = await this.client.get<T>(url, { params });
    return response.data;
  }

  async post<T, B = any>(url: string, body: B): Promise<T> {
    const response = await this.client.post<T>(url, body);
    return response.data;
  }

  async put<T, B = any>(url: string, body: B): Promise<T> {
    const response = await this.client.put<T>(url, body);
    return response.data;
  }

  async delete<T = void>(url: string): Promise<T> {
    const response = await this.client.delete<T>(url);
    return response.data;
  }
}

// 使用
const api = new ApiClient("https://api.example.com");
const users = await api.get<User[]>("/users");

通用 API 封装

响应类型

// 标准 API 响应格式
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

// 分页响应
interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  totalPages: number;
}

// 错误响应
interface ApiError {
  code: number;
  message: string;
  details?: Record<string, string[]>;
}

完整的 API 客户端

class ApiClient {
  private baseURL: string;
  private headers: Record<string, string>;

  constructor(baseURL: string) {
    this.baseURL = baseURL;
    this.headers = { "Content-Type": "application/json" };
  }

  setToken(token: string): void {
    this.headers.Authorization = `Bearer ${token}`;
  }

  private async request<T>(
    method: string,
    path: string,
    body?: any,
    params?: Record<string, string>
  ): Promise<ApiResponse<T>> {
    const url = new URL(path, this.baseURL);
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        url.searchParams.set(key, value);
      });
    }

    const response = await fetch(url.toString(), {
      method,
      headers: this.headers,
      body: body ? JSON.stringify(body) : undefined
    });

    const data: ApiResponse<T> = await response.json();

    if (!response.ok) {
      throw new ApiRequestError(data.code, data.message);
    }

    return data;
  }

  async get<T>(path: string, params?: Record<string, string>): Promise<T> {
    const response = await this.request<T>("GET", path, undefined, params);
    return response.data;
  }

  async post<T, B = any>(path: string, body: B): Promise<T> {
    const response = await this.request<T>("POST", path, body);
    return response.data;
  }

  async put<T, B = any>(path: string, body: B): Promise<T> {
    const response = await this.request<T>("PUT", path, body);
    return response.data;
  }

  async delete<T = void>(path: string): Promise<T> {
    const response = await this.request<T>("DELETE", path);
    return response.data;
  }
}

class ApiRequestError extends Error {
  constructor(
    public code: number,
    message: string
  ) {
    super(message);
    this.name = "ApiRequestError";
  }
}

资源 API 基类

abstract class ResourceApi<T, CreateDto, UpdateDto> {
  constructor(
    protected client: ApiClient,
    protected resourcePath: string
  ) {}

  async getAll(params?: Record<string, string>): Promise<T[]> {
    return this.client.get<T[]>(this.resourcePath, params);
  }

  async getById(id: number): Promise<T> {
    return this.client.get<T>(`${this.resourcePath}/${id}`);
  }

  async create(data: CreateDto): Promise<T> {
    return this.client.post<T, CreateDto>(this.resourcePath, data);
  }

  async update(id: number, data: UpdateDto): Promise<T> {
    return this.client.put<T, UpdateDto>(`${this.resourcePath}/${id}`, data);
  }

  async delete(id: number): Promise<void> {
    return this.client.delete(`${this.resourcePath}/${id}`);
  }
}

// 使用
interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserDto {
  name: string;
  email: string;
  password: string;
}

interface UpdateUserDto {
  name?: string;
  email?: string;
}

class UserApi extends ResourceApi<User, CreateUserDto, UpdateUserDto> {
  constructor(client: ApiClient) {
    super(client, "/users");
  }

  async getByEmail(email: string): Promise<User | null> {
    const users = await this.getAll({ email });
    return users[0] || null;
  }
}

错误处理

// 统一错误处理
class ApiError extends Error {
  constructor(
    public status: number,
    public code: string,
    message: string,
    public details?: Record<string, string[]>
  ) {
    super(message);
    this.name = "ApiError";
  }
}

class NetworkError extends Error {
  constructor(message: string = "网络连接失败") {
    super(message);
    this.name = "NetworkError";
  }
}

class TimeoutError extends Error {
  constructor(message: string = "请求超时") {
    super(message);
    this.name = "TimeoutError";
  }
}

// 包装请求函数
async function safeRequest<T>(
  requestFn: () => Promise<T>
): Promise<[T | null, Error | null]> {
  try {
    const data = await requestFn();
    return [data, null];
  } catch (error) {
    if (error instanceof ApiError) {
      return [null, error];
    }
    if (error instanceof TypeError && error.message === "Failed to fetch") {
      return [null, new NetworkError()];
    }
    return [null, error as Error];
  }
}

// 使用
const [users, error] = await safeRequest(() => api.get<User[]>("/users"));
if (error) {
  console.error(error.message);
} else {
  console.log(users!.length);
}

业务场景:类型安全的 REST API

// API 路由类型映射
interface ApiRoutes {
  "/users": {
    GET: { params: { page?: string; limit?: string }; response: User[] };
    POST: { body: CreateUserDto; response: User };
  };
  "/users/:id": {
    GET: { response: User };
    PUT: { body: UpdateUserDto; response: User };
    DELETE: { response: void };
  };
  "/posts": {
    GET: { params: { userId?: string }; response: Post[] };
    POST: { body: CreatePostDto; response: Post };
  };
}

// 类型安全的请求函数
type ExtractResponse<T> = T extends { response: infer R } ? R : never;
type ExtractBody<T> = T extends { body: infer B } ? B : never;

class TypedApiClient {
  async get<P extends keyof ApiRoutes>(
    path: P,
    params?: ApiRoutes[P]["GET"] extends { params: infer P } ? P : never
  ): Promise<ExtractResponse<ApiRoutes[P]["GET"]>> {
    // 实现
    return {} as any;
  }

  async post<P extends keyof ApiRoutes>(
    path: P,
    body: ExtractBody<ApiRoutes[P]["POST"]>
  ): Promise<ExtractResponse<ApiRoutes[P]["POST"]>> {
    return {} as any;
  }
}

const api = new TypedApiClient();
const users = await api.get("/users", { page: "1" }); // User[]
const user = await api.post("/users", { name: "Alice", email: "[email protected]", password: "123" }); // User

注意事项

  1. fetch 不会自动 reject 非 2xx 响应——需要手动检查 response.ok
  2. fetch 返回的 response.json()any——需要手动断言或使用泛型
  3. axios 自动 reject 非 2xx 响应——通过 try-catch 捕获错误
  4. 始终处理错误——网络请求是最容易出错的地方
  5. 使用拦截器统一处理认证和错误

扩展阅读