如何用 TypeScript 编写正确的 API 客户端
在本文中,我将详细讨论如何用 TypeScript 实现客户端 API,以便与第三方 API 以及我自己的 API 协同工作。该客户端可以与公共和受保护的端点协同工作,并且不受特定框架的约束,因此非常适合在 React、Vue、Svelte 和其他框架中使用。
创建应用程序比创建待办事项列表更复杂,我们通常需要与存储在服务器上的一些数据进行交互。这些数据可能包括由第三方 API 处理的天气预报,也可能包括客户的数据,例如他们的登录名和密码,或是商店中的购物清单。使用 SPA(单页应用程序)应用程序时,我们需要从客户端接收、修改和发送这些数据。因此,您需要某种层来负责与服务器交互。在本文中,我们将考虑使用 React 库的 API 客户端,尽管它可以安全地用于 Vue、Svelte 等框架。
为什么不在使用查询的组件中注册所有查询?
很简单:如果你更改了正在使用的 API 接口,就必须遍历所有代码,找出所有受影响的变更点。既然我们现在正在讨论这个问题,你可以尝试将这种逻辑放入 React hooks 中,但这种解决方案无法在其他使用其他框架的项目中应用。
TypeScript 实现
首先,我们将 API 所在的域放在与文件一起使用的配置中.env
:
REACT_APP_API_BASE_URL="http://localhost:8083"
export default {
get apiBaseUrl(): string {
return process.env.REACT_APP_API_BASE_URL || "";
},
}
然后,我们将编写一个抽象客户端本身,它不与该领域绑定。它需要axios和axios-extensions库才能工作。
客户端代码:
import axios, {AxiosInstance, AxiosRequestConfig} from "axios";
import {
Forbidden,
HttpError,
Unauthorized
} from '../errors';
import {Headers} from "../types";
export class ApiClient {
constructor(
private readonly baseUrl: string,
private readonly headers: Headers,
private readonly authToken: string = ""
) {}
public async get(endpoint: string = "", params?: any, signal?: AbortSignal): Promise<any> {
try {
const client = this.createClient(params);
const response = await client.get(endpoint, { signal });
return response.data;
} catch (error: any) {
this.handleError(error);
}
}
public async post(endpoint: string = "", data?: any, signal?: AbortSignal): Promise<any> {
try {
const client = this.createClient();
const response = await client.post(endpoint, data, { signal });
return response.data;
} catch (error) {
this.handleError(error);
}
}
public async uploadFile(endpoint: string = "", formData: FormData): Promise<any> {
try {
const client = this.createClient();
const response = await client.post(endpoint, formData, {
headers: {
"Content-Type": "multipart/form-data",
}
})
return response.data;
} catch (error) {
this.handleError(error);
}
}
private createClient(params: object = {}): AxiosInstance {
const config: AxiosRequestConfig = {
baseURL: this.baseUrl,
headers: this.headers,
params: params
}
if (this.authToken) {
config.headers = {
Authorization: `Bearer ${this.authToken}`,
}
}
return axios.create(config);
}
private handleError(error: any): never {
if (!error.response) {
throw new HttpError(error.message)
} else if (error.response.status === 401) {
throw new Unauthorized(error.response.data);
} else if (error.response.status === 403) {
throw new Forbidden(error.response.data);
} else {
throw error
}
}
}
客户端使用自定义类型,例如Headers
,实际上只是一个字典[key: string]: string,以及继承全局Error
类的各种错误(Unauthorized、Forbidden、HTTPError),以便将来更容易理解导致它们的原因。
该类只有三个公共方法,每次使用时都会生成一个 axios 客户端。该客户端可以通过添加带有 Bearer 令牌的标头来处理公共 API 端点和受保护的端点。客户端如何接收此令牌将在稍后讨论。get 和 post 方法都使用可选abort Signal
参数,该参数允许您根据用户的操作中断请求的发送。
在向服务器发送任何文件的情况下,客户端使用该uploadFile()
方法,向服务器发送带有Content-Type: multipart/form-data标头的请求。
为了封装创建这些客户端的逻辑,我们将编写一个工厂。
工厂代码:
import {Headers} from "../../types";
import {ApiClient} from "../../clients";
export class ApiClientFactory {
constructor(
private readonly baseUrl: string,
private readonly headers: Headers = {}
) {}
public createClient(): ApiClient {
return new ApiClient(this.baseUrl, this.headers);
}
public createAuthorizedClient(authToken: string): ApiClient {
return new ApiClient(this.baseUrl, this.headers, authToken);
}
}
它不会做任何复杂的事情:它只是创建一个常规客户端或授权客户端,并将令牌传递给构造函数。
具体实现
现在我们需要将这个抽象客户端适配到某个特定的端点。例如,让我们创建一个管理器,从服务器接收用户配置文件的最新状态:
import {ApiClientInterface} from "./clients";
import {Profile} from "./models";
export class ProfileManager {
constructor(private readonly apiClient: ApiClientInterface) {}
public async get(): Promise<Profile> {
return this.apiClient.get("");
}
}
在这个例子中,我们不关心配置文件使用的模型。我们只需假设它与服务器传输的值兼容即可。
管理器类本身使用组合并将客户端对象存储在其状态中,以便将所有 API 请求转发给它,并且如果需要,它可以向接收的值添加一些自己的逻辑(执行验证、创建自己的端点等)。
通常,API 会通过在其端点上添加特定前缀来对域逻辑进行分组。也存在 API 从一个版本迁移到新版本的情况。为了满足所有这些需求,我们将为这个特定的管理器创建一个工厂。
工厂代码:
import {ApiClientFactory} from "./clients";
import {Headers} from "../types";
import {ProfileManager} from "../ProfileManager";
export class ProfileManagerFactory {
private readonly apiClientFactory: ApiClientFactory;
constructor(baseUrl: string, headers: Headers) {
this.apiClientFactory = new ApiClientFactory(
`${baseUrl}/api/v1/profile`,
headers
);
}
public createProfileManager(authToken: string): ProfileManager {
return new ProfileManager(
this.apiClientFactory.createAuthorizedClient(authToken)
);
}
}
创建此工厂时,会将域 URL 和请求标头传递给构造函数。然后,这些参数会传递给客户端 API 工厂构造函数,并在传递的 URL 后添加 API 版本和相同的前缀(表示域逻辑的一部分)。创建用户配置文件管理器时需要授权,因此会将令牌传递给该方法,并在此基础上创建带有授权标头的客户端。
依赖注入
现在剩下的就是编写一个函数,负责在代码的任何部分(无论是 React 组件还是独立的 TypeScript 类)提供可用的配置文件管理器。它看起来会像这样:
export async function createProfileManager(): Promise<apiClient.ProfileManager> {
const factory = new apiClient.ProfileManagerFactory(apiClientConfig.apiBaseUrl, getBaseHeaders());
return factory.createProfileManager(await getAuthToken());
}
首先,在内部创建这些管理器的工厂,将服务器域和基本头转移到该工厂,如下所示:
function getBaseHeaders(): apiClient.Headers {
return {
"Accept-Language": "en"
}
}
如果需要,您可以在管理器创建功能级别添加任何您自己的标题。
getAuthToken()
我不会在本文中讨论获取 API 令牌的方法和函数的操作,因为这个主题值得单独发表。
async function getAuthToken(): Promise<string> {
// ЗThere would be a token receipt code here, but for now...
return localStorage.getItem("auth-token");
}
在组件中使用
配置文件管理器的示例如下所示:
useEffect(() => {
(async () => {
try {
await initProfile();
} catch (error: any) {
await handleError(error);
} finally {
setLoading(false);
}
})()
}, []);
const initProfile = async () => {
const manager = await createProfileManager();
const profile = await manager.get();
await dispatch(set(profile));
}
当函数在useEffect
钩子中运行时,会异步创建一个配置文件管理器,然后它会从服务器请求用户配置文件的当前状态。在本例中,我们只需将接收到的状态写入 Redux 存储,这样我们就可以处理此配置文件,而无需每次都从服务器重新请求。如果客户端发生错误,handleError()
则会启动该函数,并根据错误类型(如我之前提到的)执行相应的操作。
结果
此实现独立于您正在使用的框架,甚至可以在原生 JS(TS)上使用。您可以对其进行许多改进,例如,添加“构建器”模式来创建 API 客户端并向其传递参数、中止信号和其他内容,或者通过 JWT 令牌构建一个可变的身份验证系统。一切都由您自行决定。在下一篇文章中,我将向您介绍在客户端获取和使用 API 令牌的方法。
在Github上关注我<3
鏂囩珷鏉ユ簮锛�https://dev.to/ra1nbow1/how-to-write-the-right-api-client-in-typescript-38g3