使用 TypeScript 管理 Vue.js 中的 API 层

2025-06-08

使用 TypeScript 管理 Vue.js 中的 API 层

动机

几乎每个单页应用程序在某些时候都需要从后端获取一些数据。有时,数据来源有多种,例如 REST API、Web Sockets 等。以正确的方式管理 API 层非常重要,这样才能使其在应用程序的任何位置(无论是存储、组件还是其他类型的源文件)都简单易用。

TLDR

如果您已经具备一定的开发经验,并希望查看解决方案,这里有FancyUserCard示例。如果您觉得有些内容难以理解,可以查看详细的分步说明。

坏的

组件中 API 调用示例在组件中执行 API 调用是不好的,因为:

  • 你把你的组件变得很大,并且充满了与组件本身无关的逻辑,这违反了 SRP;
  • 相同的 API 方法可以在不同的组件中使用,这会导致代码重复并违反 DRY;
  • 您正在全局导入依赖项,这违反了 DI 原则;
  • 每当 API 发生变化时,您都需要手动更改每个需要修改的方法。

好的

为了使事情更好地运作,我们需要稍微改变我们的代码并将所有的 API 调用移到一个单独的位置。

用户.api.ts

用户API层文件在这种情况下,我们:

  • 有一个AxiosInstance配置为与/usersAPI 分支一起工作的单一代码,我们的代码就变得模块化了;
  • 将所有方法集中在一个地方,以便更容易进行更改并在不同的组件中重用它们,而无需重复代码;
  • 处理成功的请求以及失败的请求,并使我们能够根据请求状态处理错误和数据对象;
  • 为每种方法提供标准化的响应返回类型,以便我们可以以一种方式使用它们。

FancyUserCard.vue

导入 API 方法的 FancyUserCard 组件在我们的组件中:

  • 我们根本不处理 HTTP 层,所以我们的组件只负责渲染来自 API 层的数据;
  • 方法会返回错误和数据,因此如果出现问题,我们可以通知您的用户,或者只是使用方法返回的数据。

先进的

封装在类中的 API 方法最后的一些变化:

  • 移动了 API 调用方法以减少代码重复,并且所有方法都使用此私有方法调用。

其他一些想法

上面展示的方法足以处理标准的 API 层工作流程。如果您想让它更加灵活,可以考虑实现以下一些想法:

在 HTTP 层上创建抽象在 HTTP 层上创建抽象关于这个想法:

在示例中,您可以看到我们现在有了一个接口,HttpClient因此我们可以根据需要拥有任意数量的实现。如果我们有不同的 HTTP 客户端(例如axiosfetch、,它也能正常工作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);
  });


Enter fullscreen mode Exit fullscreen mode

正如您已经看到的,我们直接访问组件data()并使用全局,axios这迫使我们输入更多代码来设置请​​求配置。

待办事项列表

  1. 将代码迁移到单独的方法;
  2. then语法移至async/ await
  3. 设置axios实例;
  4. 管理方法返回类型;
  5. 将方法封装在 中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);
  });
}


Enter fullscreen mode Exit fullscreen mode

已经有所改进!现在,我们可以在需要获取 时导入此函数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);
  }
}


Enter fullscreen mode Exit fullscreen mode

如您所见,现在我们提供额外的类型并使用await来停止我们的代码运行,直到 API 调用完成。请记住,您只能在函数内部使用awaitasync

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);
  }
}


Enter fullscreen mode Exit fullscreen mode

好多了。现在,如果您的/usersAPI 分支需要更改,您只需在实例配置中重写它,它就会应用于使用 this 进行的每个调用AxiosInstance。此外,现在您还可以使用名为“拦截器”的功能,它允许您对请求/响应进行一些额外的更改,或者在发出请求或返回响应时执行逻辑。查看链接了解更多详情!

4. 管理方法返回类型

如果我告诉你,你的用户不明白是否出了问题(以及为什么出了问题)……直到你提供一些关于“出了什么问题”的信息,你会怎么想?用户体验对于保持用户满意度和改善工作流程至关重要。那么我们该怎么做呢?只需在 API 调用中同时返回dataerror即可。你也可以根据需要返回任意数量的值(如果你需要的话,对吧?):



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];
  }
}


Enter fullscreen mode Exit fullscreen mode

当我们使用它时它会是什么样子,例如在我们的created()回调中:



async created() {
  const [error, user] = await getUser(this.selectedUser);

  if (error) notifyUserAboutError(error);
  else this.user = user;
}


Enter fullscreen mode Exit fullscreen mode

因此,在这种情况下,如果发生任何错误,您将能够对此做出响应并执行一些操作,例如推送错误通知、提交错误报告或执行您在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];
  }
}


Enter fullscreen mode Exit fullscreen mode

和用法:



  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;
    }
  }


Enter fullscreen mode Exit fullscreen mode

正如您所看到的,您的请求变得越来越强大,但与此同时,您可以让您的组件摆脱该逻辑并仅处理您需要的细节。

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];
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

用法如下:



const axiosInstance = axios.create({
  baseURL: "https://api.fancy-host.com/v1/users"
});

export const userService = new UserService(axiosInstance);


Enter fullscreen mode Exit fullscreen mode

在这种情况下,您不会公开您的信息AxiosInstance,而仅通过服务公共 API 提供访问权限。

结论

希望本文对您有所帮助。如果您有其他想法,或者对本文内容有任何疑问,请随时留言。我将很快更新这篇文章,详细介绍问题、解决方案和重构过程。
谢谢!

鏂囩珷鏉ユ簮锛�https://dev.to/blindkai/managing-api-layers-in-vue-js-with-typescript-hno
PREV
无需框架即可构建响应式网站什么是响应式 Web 开发?
NEXT
自定义省略的 Git Bash shell 行