使用 TypeScript 管理 Vue.js 中的 API 层
动机
几乎每个单页应用程序在某些时候都需要从后端获取一些数据。有时,数据来源有多种,例如 REST API、Web Sockets 等。以正确的方式管理 API 层非常重要,这样才能使其在应用程序的任何位置(无论是存储、组件还是其他类型的源文件)都简单易用。
TLDR
如果您已经具备一定的开发经验,并希望查看解决方案,这里有FancyUserCard
示例。如果您觉得有些内容难以理解,可以查看详细的分步说明。
坏的
- 你把你的组件变得很大,并且充满了与组件本身无关的逻辑,这违反了 SRP;
- 相同的 API 方法可以在不同的组件中使用,这会导致代码重复并违反 DRY;
- 您正在全局导入依赖项,这违反了 DI 原则;
- 每当 API 发生变化时,您都需要手动更改每个需要修改的方法。
好的
为了使事情更好地运作,我们需要稍微改变我们的代码并将所有的 API 调用移到一个单独的位置。
用户.api.ts
- 有一个
AxiosInstance
配置为与/users
API 分支一起工作的单一代码,我们的代码就变得模块化了; - 将所有方法集中在一个地方,以便更容易进行更改并在不同的组件中重用它们,而无需重复代码;
- 处理成功的请求以及失败的请求,并使我们能够根据请求状态处理错误和数据对象;
- 为每种方法提供标准化的响应返回类型,以便我们可以以一种方式使用它们。
FancyUserCard.vue
- 我们根本不处理 HTTP 层,所以我们的组件只负责渲染来自 API 层的数据;
- 方法会返回错误和数据,因此如果出现问题,我们可以通知您的用户,或者只是使用方法返回的数据。
先进的
- 移动了 API 调用方法以减少代码重复,并且所有方法都使用此私有方法调用。
其他一些想法
上面展示的方法足以处理标准的 API 层工作流程。如果您想让它更加灵活,可以考虑实现以下一些想法:
在 HTTP 层上创建抽象
关于这个想法:
在示例中,您可以看到我们现在有了一个接口,HttpClient
因此我们可以根据需要拥有任意数量的实现。如果我们有不同的 HTTP 客户端(例如axios
)fetch
、,它也能正常工作ky
。如果我们需要从一个客户端迁移到另一个客户端,我们只需HttpClient
在一个地方重写实现,它将自动应用于我们使用服务的任何地方;
创建工厂
关于想法:
如果您有多个不同的数据源,可以使用某种工厂来创建具有所需实现的实例,而无需显式声明类。在这种情况下,您只需提供一个契约接口,然后根据需要实现每个 API 方法。
关于问题
众所周知,在组件中处理 API 调用是有害的,因为每当代码发生变化时,您都需要做大量工作来维护代码的正常运行。此外,由于组件和 API 直接且深度耦合,测试起来也相当困难。我们希望在编写代码时避免这些问题,所以让我们通过示例来了解一下。
例子
这是 API 调用初始示例的代码。为了简单起见,我们省略其他代码,只关注方法本身。
axios
.get<User>(`https://api.fancy-host.com/v1/users/${this.userId}`)
.then((response) => {
this.user = response.data;
})
.catch((error) => {
console.error(error);
});
正如您已经看到的,我们直接访问组件data()
并使用全局,axios
这迫使我们输入更多代码来设置请求配置。
待办事项列表
- 将代码迁移到单独的方法;
- 从
then
语法移至async
/await
; - 设置
axios
实例; - 管理方法返回类型;
- 将方法封装在 中
Class
。
重构
1. 将代码迁移到单独的方法
首先,不要将我们的代码移动到单独的文件,并简单地导出一个接受userId
输入参数的函数,user
如果调用成功则返回对象:
export function getUser(userId: number) {
axios
.get<User>(`https://api.fancy-host.com/v1/users/${userId}`)
.then((response) => {
return response.data;
})
.catch((error) => {
console.error(error);
});
}
已经有所改进!现在,我们可以在需要获取 时导入此函数User
。我们只需要指定userId
就可以了。
2. 从then
语法移至async
/await
在现实世界中,经常会出现需要进行顺序调用的情况。例如,当你执行 fetch 操作时,user
你可能想获取与用户相关的帖子或评论信息,对吧?有时,你希望并行执行请求,而这在实现上会非常棘手.then
。那么,我们为什么不改进它呢?
export async function getUser(userId: number): Promise<User | undefined> {
try {
const { data } = await axios.get<User>(`https://api.fancy-host.com/v1/users/${userId}`);
return data;
} catch (error) {
console.error(error);
}
}
如您所见,现在我们提供额外的类型并使用await
来停止我们的代码运行,直到 API 调用完成。请记住,您只能在函数内部使用await
async
。
3.设置axios
实例;
好的,现在最长的一行是包含端点 URL 的那行。你的服务器主机可能不会经常更改,最好将 API 分支设置在一个地方,所以让我们开始吧:
const axiosInstance = axios.create({
baseURL: "https://api.fancy-host.com/v1/users"
});
export async function getUser(userId: number): Promise<User | undefined> {
try {
const { data } = await axiosInstance.get<User>(`/users/${userId}`);
return data;
} catch (error) {
console.error(error);
}
}
好多了。现在,如果您的/users
API 分支需要更改,您只需在实例配置中重写它,它就会应用于使用 this 进行的每个调用AxiosInstance
。此外,现在您还可以使用名为“拦截器”的功能,它允许您对请求/响应进行一些额外的更改,或者在发出请求或返回响应时执行逻辑。查看链接了解更多详情!
4. 管理方法返回类型
如果我告诉你,你的用户不明白是否出了问题(以及为什么出了问题)……直到你提供一些关于“出了什么问题”的信息,你会怎么想?用户体验对于保持用户满意度和改善工作流程至关重要。那么我们该怎么做呢?只需在 API 调用中同时返回data
和error
即可。你也可以根据需要返回任意数量的值(如果你需要的话,对吧?):
export type APIResponse = [null, User] | [Error];
export async function getUser(userId: number): Promise<APIResponse> {
try {
const { data } = await axiosInstance.get<User>(`/${userId}`);
return [null, data];
} catch (error) {
console.error(error);
return [error];
}
}
当我们使用它时它会是什么样子,例如在我们的created()
回调中:
async created() {
const [error, user] = await getUser(this.selectedUser);
if (error) notifyUserAboutError(error);
else this.user = user;
}
因此,在这种情况下,如果发生任何错误,您将能够对此做出响应并执行一些操作,例如推送错误通知、提交错误报告或执行您在notifyUserAboutError
方法中添加的任何其他逻辑。否则,如果一切顺利,您可以简单地将用户对象放入Vue
组件并渲染新的信息。
此外,如果您需要返回其他信息(例如,状态代码以指示是否是请求失败的400 Bad Request
情况401 Unautorized
,或者如果一切正常,您是否想要获取一些响应标头),您可以在方法返回中添加一个对象:
export type Options = { headers?: Record<string, any>; code?: number };
export type APIResponse = [null, User, Options?] | [Error, Options?];
export async function getUser(userId: number): Promise<APIResponse> {
try {
const { data, headers } = await axiosInstance.get<User>(`/${userId}`);
return [null, data, { headers }];
} catch (error) {
console.error(error);
return [error, error.response?.status];
}
}
和用法:
async created() {
const [error, user, options] = await getUser(this.selectedUser);
if (error) {
notifyUserAboutError(error);
if (options?.code === 401) goToAuth();
if (options?.code === 400) notifyBadRequest(error);
} else {
this.user = user;
const customHeader = options?.headers?.customHeader;
}
}
正如您所看到的,您的请求变得越来越强大,但与此同时,您可以让您的组件摆脱该逻辑并仅处理您需要的细节。
5. 将方法封装在Class
现在是时候进行最后的润色了。我们的代码已经运行良好,但我们可以让它变得更好。例如,在某些情况下,我们想要测试组件如何与其他层交互。同时,我们不想执行真正的请求,只要确保它们完全正确就足够了。为了实现这个结果,我们希望能够模拟我们的 HTTP 客户端。为了实现这一点,我们希望将一个模拟实例“注入”到我们的模块中,很难想象还有比使用Class
及其更好的方法来实现这一点constructor
。
export class UserService {
constructor(private httpClient: AxiosInstance) {}
async getUser(userId: number): Promise<APIResponse> {
try {
const { data } = await this.httpClient.get<User>(`/${userId}`);
return [null, data];
} catch (error) {
console.error(error);
return [error];
}
}
}
用法如下:
const axiosInstance = axios.create({
baseURL: "https://api.fancy-host.com/v1/users"
});
export const userService = new UserService(axiosInstance);
在这种情况下,您不会公开您的信息AxiosInstance
,而仅通过服务公共 API 提供访问权限。
结论
希望本文对您有所帮助。如果您有其他想法,或者对本文内容有任何疑问,请随时留言。我将很快更新这篇文章,详细介绍问题、解决方案和重构过程。
谢谢!