MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript封装RESTful API客户端实践

2022-03-145.3k 阅读

1. 理解 RESTful API

RESTful(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序。它基于 HTTP 协议,通过使用不同的 HTTP 方法(GET、POST、PUT、DELETE 等)来对资源进行操作。例如,GET 方法通常用于获取资源,POST 方法用于创建新资源,PUT 方法用于更新现有资源,DELETE 方法用于删除资源。

假设我们有一个管理用户信息的 RESTful API,其资源可能如下:

  • 获取所有用户GET /users
  • 获取单个用户GET /users/{id}
  • 创建新用户POST /users
  • 更新用户PUT /users/{id}
  • 删除用户DELETE /users/{id}

2. TypeScript 的优势

TypeScript 是 JavaScript 的超集,它增加了静态类型检查,使得代码更加健壮。在封装 RESTful API 客户端时,TypeScript 有以下优势:

  • 类型安全:可以在编译时发现类型错误,避免运行时出现难以调试的错误。例如,定义 API 响应的数据类型,确保处理数据时不会因为类型不匹配而出错。
  • 代码可读性和可维护性:通过类型注释,代码的意图更加清晰。对于大型项目,不同开发人员可以更容易理解和维护代码。

3. 封装基础的 HTTP 请求函数

我们可以使用 fetch 来发起 HTTP 请求。在 TypeScript 中,首先定义一些类型来辅助我们的操作。

// 定义通用的响应类型
interface HttpResponse<T> {
  status: number;
  data: T;
}

// 封装基本的 HTTP 请求函数
async function httpRequest<T>(url: string, options: RequestInit = {}): Promise<HttpResponse<T>> {
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  const data = await response.json() as T;
  return {
    status: response.status,
    data
  };
}

4. 封装 GET 请求

基于上面的 httpRequest 函数,我们可以封装 GET 请求。

// 封装 GET 请求
async function get<T>(url: string, params?: Record<string, string | number | boolean>): Promise<HttpResponse<T>> {
  const queryString = new URLSearchParams();
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      queryString.append(key, String(value));
    });
  }
  const finalUrl = `${url}${queryString ? `?${queryString.toString()}` : ''}`;
  return httpRequest<T>(finalUrl, {
    method: 'GET'
  });
}

5. 封装 POST 请求

接下来封装 POST 请求,用于创建新资源。

// 封装 POST 请求
async function post<T, U>(url: string, data: U): Promise<HttpResponse<T>> {
  return httpRequest<T>(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });
}

6. 封装 PUT 请求

PUT 请求用于更新现有资源。

// 封装 PUT 请求
async function put<T, U>(url: string, data: U): Promise<HttpResponse<T>> {
  return httpRequest<T>(url, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });
}

7. 封装 DELETE 请求

DELETE 请求用于删除资源。

// 封装 DELETE 请求
async function del<T>(url: string): Promise<HttpResponse<T>> {
  return httpRequest<T>(url, {
    method: 'DELETE'
  });
}

8. 定义 API 接口

假设我们有一个用户管理的 API,我们可以定义接口来描述 API 的请求和响应。

// 用户数据类型
interface User {
  id: number;
  name: string;
  email: string;
}

// 获取所有用户
async function getUsers(): Promise<HttpResponse<User[]>> {
  return get<User[]>('/users');
}

// 获取单个用户
async function getUser(id: number): Promise<HttpResponse<User>> {
  return get<User>(`/users/${id}`);
}

// 创建新用户
async function createUser(user: Omit<User, 'id'>): Promise<HttpResponse<User>> {
  return post<User, Omit<User, 'id'>>('/users', user);
}

// 更新用户
async function updateUser(user: User): Promise<HttpResponse<User>> {
  return put<User, User>(`/users/${user.id}`, user);
}

// 删除用户
async function deleteUser(id: number): Promise<HttpResponse<void>> {
  return del<void>(`/users/${id}`);
}

9. 使用封装的 API 客户端

在实际应用中,我们可以这样使用封装好的 API 客户端。

async function main() {
  try {
    // 获取所有用户
    const allUsersResponse = await getUsers();
    console.log('All users:', allUsersResponse.data);

    // 创建新用户
    const newUser: Omit<User, 'id'> = {
      name: 'John Doe',
      email: 'johndoe@example.com'
    };
    const createUserResponse = await createUser(newUser);
    console.log('Created user:', createUserResponse.data);

    // 获取单个用户
    const userId = createUserResponse.data.id;
    const singleUserResponse = await getUser(userId);
    console.log('Single user:', singleUserResponse.data);

    // 更新用户
    const updatedUser = {
     ...singleUserResponse.data,
      name: 'Jane Doe'
    };
    const updateUserResponse = await updateUser(updatedUser);
    console.log('Updated user:', updateUserResponse.data);

    // 删除用户
    await deleteUser(userId);
    console.log('User deleted successfully');
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

10. 错误处理和增强

在实际应用中,错误处理至关重要。我们可以对 httpRequest 函数进行增强,使其能够处理更详细的错误信息。

// 定义错误类型
interface HttpError extends Error {
  status: number;
}

// 增强的 httpRequest 函数
async function httpRequest<T>(url: string, options: RequestInit = {}): Promise<HttpResponse<T>> {
  const response = await fetch(url, options);
  if (!response.ok) {
    const error: HttpError = new Error(`HTTP error! status: ${response.status}`) as HttpError;
    error.status = response.status;
    throw error;
  }
  const data = await response.json() as T;
  return {
    status: response.status,
    data
  };
}

然后在调用 API 的地方进行错误处理。

async function main() {
  try {
    // 获取所有用户
    const allUsersResponse = await getUsers();
    console.log('All users:', allUsersResponse.data);
  } catch (error) {
    if ((error as HttpError).status === 404) {
      console.error('Resource not found');
    } else {
      console.error('Error:', error);
    }
  }
}

main();

11. 认证和授权

在实际的 RESTful API 中,认证和授权是常见的需求。我们可以通过在请求头中添加认证信息来实现。

// 假设我们使用 JWT 认证
function setAuthHeader(options: RequestInit, token: string) {
  if (!options.headers) {
    options.headers = {};
  }
  options.headers.Authorization = `Bearer ${token}`;
  return options;
}

// 封装带认证的 GET 请求
async function authenticatedGet<T>(url: string, token: string, params?: Record<string, string | number | boolean>): Promise<HttpResponse<T>> {
  const options = setAuthHeader({
    method: 'GET'
  }, token);
  const queryString = new URLSearchParams();
  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      queryString.append(key, String(value));
    });
  }
  const finalUrl = `${url}${queryString ? `?${queryString.toString()}` : ''}`;
  return httpRequest<T>(finalUrl, options);
}

12. 处理响应缓存

为了提高性能,我们可以实现响应缓存。可以使用一个简单的内存缓存来存储 API 响应。

// 缓存对象
const responseCache: Record<string, HttpResponse<any>> = {};

// 封装带缓存的 GET 请求
async function cachedGet<T>(url: string, params?: Record<string, string | number | boolean>): Promise<HttpResponse<T>> {
  const cacheKey = `${url}${params ? `?${new URLSearchParams(params).toString()}` : ''}`;
  if (responseCache[cacheKey]) {
    return responseCache[cacheKey] as HttpResponse<T>;
  }
  const response = await get<T>(url, params);
  responseCache[cacheKey] = response;
  return response;
}

13. 处理分页数据

许多 RESTful API 使用分页来返回大量数据。我们可以在封装的客户端中处理分页逻辑。

// 定义分页响应类型
interface PaginatedResponse<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
}

// 获取分页用户数据
async function getPaginatedUsers(page: number, limit: number): Promise<HttpResponse<PaginatedResponse<User>>> {
  return get<PaginatedResponse<User>>('/users', { page, limit });
}

14. 处理复杂的 API 响应结构

有些 API 可能返回复杂的响应结构,例如包含嵌套数据或者元数据。我们需要在 TypeScript 中准确地定义这些类型。

// 假设 API 响应包含元数据
interface ApiResponseWithMetadata<T> {
  data: T;
  metadata: {
    timestamp: string;
    version: string;
  };
}

// 获取带有元数据的用户数据
async function getUsersWithMetadata(): Promise<HttpResponse<ApiResponseWithMetadata<User[]>>> {
  return get<ApiResponseWithMetadata<User[]>>('/users/metadata');
}

15. 与 React 或其他框架集成

在实际项目中,我们通常会将 API 客户端与前端框架(如 React)集成。在 React 中,我们可以使用 useEffect 钩子来发起 API 请求。

import React, { useEffect, useState } from'react';

function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      try {
        const response = await getUsers();
        setUsers(response.data);
      } catch (error) {
        setError((error as HttpError).message);
      } finally {
        setLoading(false);
      }
    };
    fetchUsers();
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }
  if (error) {
    return <div>Error: {error}</div>;
  }

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

16. 优化和性能提升

  • 批量请求:如果需要获取多个相关资源,可以尝试将多个请求合并为一个。例如,使用 GraphQL 或者在 RESTful API 中提供批量获取的接口,并在客户端进行相应封装。
  • 请求优先级:对于一些关键请求(如用户登录信息更新),可以设置较高的优先级,确保其优先被处理。在 fetch 中,可以通过设置 priority 选项(在支持的浏览器中)来实现。
// 示例:设置高优先级的 POST 请求
async function highPriorityPost<T, U>(url: string, data: U): Promise<HttpResponse<T>> {
  return httpRequest<T>(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data),
    priority: 'high' as RequestPriority
  });
}

17. 测试封装的 API 客户端

对于我们封装的 API 客户端,进行单元测试和集成测试是非常重要的。可以使用 Jest 来编写测试用例。

import { get, post, put, del } from './apiClient';

describe('API Client', () => {
  // 模拟 fetch 响应
  global.fetch = jest.fn(() =>
    Promise.resolve({
      ok: true,
      json: () => Promise.resolve({})
    })
  ) as jest.Mock;

  describe('GET 请求测试', () => {
    it('应该正确发起 GET 请求', async () => {
      const response = await get<any>('/test');
      expect(response.status).toBe(200);
    });
  });

  describe('POST 请求测试', () => {
    it('应该正确发起 POST 请求', async () => {
      const response = await post<any, any>('/test', {});
      expect(response.status).toBe(200);
    });
  });

  describe('PUT 请求测试', () => {
    it('应该正确发起 PUT 请求', async () => {
      const response = await put<any, any>('/test', {});
      expect(response.status).toBe(200);
    });
  });

  describe('DELETE 请求测试', () => {
    it('应该正确发起 DELETE 请求', async () => {
      const response = await del<any>('/test');
      expect(response.status).toBe(200);
    });
  });
});

通过以上步骤,我们详细地实践了如何使用 TypeScript 封装 RESTful API 客户端,从基础的 HTTP 请求封装,到处理认证、缓存、分页等复杂场景,以及与前端框架的集成和测试,全面覆盖了实际项目中可能遇到的各种情况。在实际应用中,可以根据具体需求对封装进行进一步的扩展和优化。