管理 API 调用的简单方法

2025-05-25

管理 API 调用的简单方法

在我的文章《构建可扩展的前端项目》中,我们探讨了如何组织前端代码库,以便团队更轻松地实现扩展并取得成功。在本文中,我们将深入探讨代码组织的服务层。具体来说,我们将研究一种简单的解决方案,用于管理第三方 API 或我们自己的数据源,从而避免 API 随时间变化而导致的代码库管理方面的一些困扰。

当我们刚开始构建功能时,大多数人倾向于将所有功能逻辑都转储到一个组件中。数据库调用、状态管理以及所有管理或显示我们要呈现给最终用户的数据的子组件都位于此处。这样做的结果是,我们开始创建一组非常臃肿的文件,这些文件使用、管理和呈现所有逻辑,并且随着业务逻辑的增加,这些逻辑变得越来越复杂。最初可能是简单的 CRUD(创建、读取、更新、删除)操作,最终将不可避免地发展成众多专门的功能和相互交织的业务逻辑。如果我们在代码架构设计过程中不够谨慎,我们可能会发现自己陷入了混乱的功能依赖关系中,甚至会害怕重构过程,因为我们不想犯下任何一个可能需要我们周末加班才能修复的错误。

避免混乱

我们可以避免这种业务逻辑混乱,其中一点就是不要将 API 调用直接硬编码到组件中。我们的目标是将所有与 API 逻辑相关的内容抽象到服务层,从而使组件更加精简和易于维护。这个概念与 Dan Abramov 的文章“展示组件和容器组件”相呼应,并且在我们的前端框架中创建一个模型/服务层,将大部分业务逻辑从可复用组件中抽象出来。

以下是您可以开始使用的简单示例:

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

let API_URL_TASKS = 'https://url.com/api/v1/tasks';

export function Tasks() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    _getTasks();
  }, []);

  function _getTasks() {
    axios
      .get(API_URL_TASKS)
      .then((res) => {
        let arr = _parseTasks(res.results.data);
        setTasks(arr);
      })
      .catch((err) => {
        _handleError(err, type);
      });
  }

  function _parseTasks(tasks) {
    return tasks.map((task) => {
      // Parse task information
      return task;
    });
  }

  function _createTask(task) {
    axios
      .post(url, task)
      .then((res) => {
        _handleSuccess(res, 'post');
        // etc...
      })
      .catch((err) => {
        _handleError(err, 'post');
      });
  }

  function _updateTask(task) {
    let url = `${API_URL_TASKS}/${id}`;
    axios
      .patch(url, task)
      .then((res) => {
        _handleSuccess(res, 'patch');
        // etc...
      })
      .catch((err) => {
        _handleError(err, 'patch');
      });
  }

  function _removeTask(id) {
    let url = `${API_URL_TASKS}/${id}`;
    axios
      .delete(url)
      .then((res) => {
        _handleSuccess(res, 'delete');
        // etc...
      })
      .catch((err) => {
        _handleError(err, 'delete');
      });
  }

  function _handleSuccess(response, type) {
    // success message
    // actions against state with type
  }

  function _handleError(error, type) {
    // error message
    // actions based on type
    // etc...
  }

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

Enter fullscreen mode Exit fullscreen mode

如您所见,我们组件的数据流与其可能需要的一个或多个 API 端点直接相关,并被硬编码。如果您随着时间的推移对许多组件执行此操作,并且您的 API 需求从服务器或第三方 API 发生变化,那么您现在就陷入了一个痛苦的过程:为了避免最终用户的代码和界面出现问题,您必须查找所有需要更改的实例。因此,我们将在服务层中创建一些文件结构,以便更轻松地维护随时间推移的更改。

my-app 
└── src
    ├── components
    ├── views
    |   └── tasks
    └── services
        ├── api
        |   ├── tasks
        |   └── utilities
        ├── model
        |   └── task
        └── etc...
Enter fullscreen mode Exit fullscreen mode

服务实用程序

在 services 文件夹中,我们将创建一些实用程序,以使我们的 API 可重用,并对所有组件和团队成员实现标准化。在此示例中,我们将使用 JavaScript axios 库和 JavaScript 类来创建 API 实用程序。

services
└── api
    └── utilities
        ├── core.js
        ├── index.js
        ├── provider.js
        └── response.js
Enter fullscreen mode Exit fullscreen mode

我们将重点关注三个主要文件:

  1. provider.js - 定义 axios 或任何 api 库如何连接数据库并将我们的响应数据连接回任何连接的文件或组件。
  2. core.js - 定义一个可复用的类,该类使用 provider.js,并为每个 API 端点集合定义选项。由于它是一个构造函数,我们可以根据需要在各个 API 集合上扩展它的功能,同时仍然保持大多数代码的一致性。
  3. response.js - 处理响应解析、错误处理、日志记录等的中间件...

Provider.js

// provider.js

import axios from 'axios'; 
import { handleResponse, handleError } from './response'; 

// Define your api url from any source.
// Pulling from your .env file when on the server or from localhost when locally
const BASE_URL = 'http://127.0.0.1:3333/api/v1'; 

/** @param {string} resource */ 
const getAll = (resource) => { 
  return axios 
    .get(`${BASE_URL}/${resource}`) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {string} id */ 
const getSingle = (resource, id) => { 
  return axios 
    .get(`${BASE_URL}/${resource}/${id}`) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {object} model */ 
const post = (resource, model) => { 
  return axios 
    .post(`${BASE_URL}/${resource}`, model) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {object} model */ 
const put = (resource, model) => { 
  return axios 
    .put(`${BASE_URL}/${resource}`, model) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {object} model */ 
const patch = (resource, model) => { 
  return axios 
    .patch(`${BASE_URL}/${resource}`, model) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

/** @param {string} resource */ 
/** @param {string} id */ 
const remove = (resource, id) => { 
  return axios 
    .delete(`${BASE_URL}/${resource}`, id) 
    .then(handleResponse) 
    .catch(handleError); 
}; 

export const apiProvider = { 
  getAll, 
  getSingle, 
  post, 
  put, 
  patch, 
  remove, 
};
Enter fullscreen mode Exit fullscreen mode

Core.js

在这个构造函数类中,我们可以定义哪些基础 API 资源将被使用。我们还可以在每个 API 实用程序中扩展该类,以包含 API 表独有的自定义端点,而无需在远离此文件的代码库中创建意外的一次性解决方案。

// core.js

import apiProvider from './provider';

export class ApiCore {
  constructor(options) {
    if (options.getAll) {
      this.getAll = () => {
        return apiProvider.getAll(options.url);
      };
    }

    if (options.getSingle) {
      this.getSingle = (id) => {
        return apiProvider.getSingle(options.url, id);
      };
    }

    if (options.post) {
      this.post = (model) => {
        return apiProvider.post(options.url, model);
      };
    }

    if (options.put) {
      this.put = (model) => {
        return apiProvider.put(options.url, model);
      };
    }

    if (options.patch) {
      this.patch = (model) => {
        return apiProvider.patch(options.url, model);
      };
    }

    if (options.remove) {
      this.remove = (id) => {
        return apiProvider.remove(options.url, id);
      };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Response.js

将其分开保存是为了保持文件精简,并允许您清晰地分离所有 API 调用的响应和错误逻辑。您可能希望在此处记录错误,或者根据响应标头创建自定义授权操作。

// response.js

export function handleResponse(response) {
  if (response.results) {
    return response.results;
  }

  if (response.data) {
    return response.data;
  }

  return response;
}

export function handleError(error) {
  if (error.data) {
    return error.data;
  }
  return error;
}
Enter fullscreen mode Exit fullscreen mode

单独的 API

我们现在可以扩展我们的基本 api 类来利用将用于任何 api 集合的所有 api 配置。

// Task API

const url = 'tasks';
const plural = 'tasks';
const single = 'task';

// plural and single may be used for message logic if needed in the ApiCore class.

const apiTasks = new ApiCore({
  getAll: true,
  getSingle: true,
  post: true,
  put: false,
  patch: true,
  delete: false,
  url: url,
  plural: plural,
  single: single
});

apiTasks.massUpdate = () => {
  // Add custom api call logic here
}

export apiTasks;
Enter fullscreen mode Exit fullscreen mode

实施我们的变革

现在我们已经完成了设置,可以根据需要将 API 调用导入并集成到多个组件中。以下是更新后的 Task 组件,其中包含我们的更改。

import React, { useEffect } from 'react';

import { apiTasks } from '@/services/api';

export function Tasks() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    _getTasks();
  }, []);

  function _getTasks() {
    apiTasks.getAll().then((res) => {
      let arr = _parseTasks(res.results.data);
      setTasks(arr);
    });
  }

  function _parseTasks(tasks) {
    return tasks.map((task) => {
      // Parse task information
      return task;
    });
  }

  function _createTask(task) {
    apiTasks.post(task).then((res) => {
      // state logic
    });
  }

  function _updateTask(task) {
    apiTasks.patch(task).then((res) => {
      // state logic
    });
  }

  function _removeTask(id) {
    apiTasks.remove(id).then((res) => {
      // state logic
    });
  }

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

Enter fullscreen mode Exit fullscreen mode

结论

通过将少量代码提取到可复用的服务实用程序中,我们的应用现在可以更轻松地管理 API 变更。现在可以在一个位置处理失败的 API 调用,轻松跟踪其实现,并且组件依赖关系可以快速更新以反映数据流和操作的变化。我希望这能帮助您管理 API 结构,使您的代码不仅能够长期可持续,而且随着代码库和团队的增长,也易于管理和理解!

以下是本文讨论的文件集合的链接:Gist Link


如果您觉得本文有帮助或有用,请分享💓、🦄或🔖。谢谢!

文章来源:https://dev.to/mmcshinsky/a-simple-approach-to-managing-api-calls-1lo6
PREV
构建可扩展的前端项目
NEXT
你的 SSR 很慢,而且你的开发者工具在欺骗你