При работе над проектом React, который подключается к внутреннему серверу, нам необходимо учитывать сложность API, предоставляемых внутренним сервером. Количество этих API может увеличиваться все больше по мере того, как мы добавляем в систему новые функции. Поэтому нам необходимо организовать их таким образом, чтобы мы могли обслуживать, контролировать, получать к ним доступ и легко перехватывать их. Эта методология также помогает нам узнать, где именно получить доступ к API, как его поддерживать и какие данные он должен принимать или отправлять с телом или URL-адресом.

В этой статье мы обсудим хорошо структурированный и организованный подход к управлению API в проекте React. Мы также рассмотрим уникальный способ собрать все API в одном месте, что позволит нам глобализировать наши действия против них.

Почему мы хотим его использовать?

Давайте рассмотрим сценарий, в котором у нас есть проект React, который подключается к внутреннему серверу REST API. Этот сервер постоянно растет по мере того, как мы добавляем новые функции. Без надлежащего планирования или структуры для определения API в нашем проекте мы можем оказаться в ситуации, когда не знаем, какой API использовать или поддерживать.

Например, предположим, что мы используем компонент, который использует около восьми API с сервера, и эти восемь API поступают от восьми разных контроллеров. . В этом случае данные из этих API могут существенно отличаться от одного API к другому, включая базовый URL, маршрут и метод HTTP-запроса. >

В этом случае нам нужен способ организовать использование этих API и точно знать, какой API использовать и какие данные отправлять.

Как использовать этот подход?

Первым шагом в организации наших API в нашем проекте React является обеспечение легкого доступа к ним. Это означает, что всякий раз, когда мы хотим использовать API, мы должны иметь возможность быстро найти его, не потерявшись. Для этого нам нужно установить структуру папок для наших API.

Делать HTTP-запросы непосредственно внутри компонента не считается хорошей практикой. Вместо этого мы должны сосредоточить код нашего компонента на его собственной логике. Поэтому нам необходимо создать специальное пространство для наших API, и это пространство должно состоять из хорошо организованной структуры папок, расположенных рядом с компонентами каталог.

Мы можем думать о структуре каталогов для наших API во внешнем интерфейсе так же, как мы делаем это для внутренней части. В бэкенде мы обычно разделяем наши серверные модули по разным каталогам и называем их «контроллеры». Например, у нас может быть контроллер «Аутентификация», контроллер «Пользователи», контроллер «Products и т. д. Каждый контроллер содержит свою собственную логику, и, хотя они могут зависеть друг от друга, они в основном независимы. Когда бэкенд-разработчику необходимо поддерживать API входа в систему, он точно знает, куда идти, он может просто перейти к контроллеру аутентификации, перейти к маршрут входа и проверьте функцию, соответствующую этому API, чтобы внести необходимые изменения.

Организовав наши интерфейсные API аналогичным образом, мы можем создать хорошо структурированный каталог API. Например, мы можем сгруппировать все API, связанные с проверкой подлинности, в файл под названием "authentication" и сделать то же самое для пользователи и продукты. Таким образом, если нам нужно изменить данные, которые принимает API входа, мы можем просто перейти к файлу аутентификации и внести необходимые изменения.

Итак, если мы говорим, что хотим применить наш пример выше на основе нашей структуры, тогда структура папок может выглядеть следующим образом:

После создания структуры папок давайте глубже заглянем внутрь этих файлов. Обычно эти файлы обрабатывают HTTP-запросы с помощью внешнего или внутреннего решения, например библиотеки Axios или функции Fetch. Содержимое этих файлов будет состоять из нескольких функций, использующих различные методы HTTP, такие как GET, POST, DELETE, PATCH и т. д. Эти функции также могут принимать параметры, то есть данные, которые мы отправляем на сервер через тело или URL-адрес запроса.

Итак, если мы посмотрим на файл authentication.js, то увидим что-то вроде этого:

import axios from "axios";


const login = async (email, password) => {
   try {
       const response = await axios.post('http://localhost:3000/api/auth/login', {email, password})
       return response;
   } catch(err) {
       console.log('API error', err)
   }
}

Это одна из функций, существующих в файле authentication.js. Как вы, возможно, знаете, он отвечает за API входа. Это асинхронная функция, которая возвращает Promise и принимает два параметра: адрес электронной почты и пароль. Мы также видим, что код заключен в блок try-catch для обработки ошибок. Наконец, API отправляется с использованием метода POST и внешней библиотеки, которой является Axios. Мы можем называть эту функцию «The Endpoint Builder».

Если мы хотим использовать этот API в нашем компоненте, мы можем просто импортировать его и вызвать. Мы можем гарантировать, что логика и обработка API входа теперь находятся в правильном месте, а не в одном месте, которое является компонентом.

Кроме того, мы можем использовать тот же процесс для других функций, таких как Регистрация. Мы можем добавить новый построитель API, который принимает регистрационные данные и реализует их таким же образом.

** Обратите внимание, что можно удалить ключевое слово "async" и использовать его как обычную функцию. Однако в некоторых случаях нам может потребоваться извлечь данные из ответа и вернуть их как обещание. Вот почему вы можете увидеть асинхронные функции при вызове API. Как показано в двух примерах ниже, существует два способа использования Конструктора конечных точек:

1 — Способ извлечения данных:

import axios from "axios";


const login = async (email, password) => {
   try {
       const {token} = await axios.post('http://localhost:3000/api/auth/login', {email, password})
       return token;
   } catch(err) {
       console.log('API error', err)
   }
}

2. Мы можем просто вызвать API и вернуться, поэтому он возвращается как promise

import axios from "axios";


const login =  (email, password) => {
   try {
       return axios.post('http://localhost:3000/api/auth/login', {email, password})
   } catch(err) {
       console.log('API error', err)
   }
}

Обратите внимание, что в обоих случаях функция вернет Promise, что является важным аспектом нашего подхода.

Теперь, когда мы создали эти функции, мы видим, что будет много дублирований, таких как базовый URL и controller, с которым мы имеем дело. В наших Endpoint Builders мы видим, что «localhost:3000/api» и «/auth» будут быть дублированы, даже для регистрации, так как они оба находятся внутри одного и того же контроллера в бэкенде.

Представьте себе сценарий, в котором у нас есть около 20 Endpoint Builders; мы столкнемся с многочисленными дубликатами этих базовых URL-адресов и контроллеров.

Чтобы решить эту проблему, нам нужно внести небольшое изменение в нашу структуру папок. Теперь нам нужно создать новый файл с именем «baseURLs». В этом файле будут константы, представляющие имена контроллеров.

Чтобы дополнительно упорядочить файлы Endpoint Builders, мы можем переместить их в каталог с именем endpoints. Таким образом, структура будет выглядеть так:

Теперь давайте подробнее рассмотрим файл «baseURLs». Как упоминалось ранее, этот файл будет содержать имена контроллеров в виде экспортированных констант. Мы можем импортировать эти константы и использовать их в разных местах без дублирования. Содержимое файла в нашем случае может выглядеть так:

export const AUTH = '/auth'

Как мы видим, мы экспортировали константу, содержащую имя контроллера. Теперь мы можем импортировать эту константу в файл построителя конечных точек аутентификации и добавить ее в маршрут отправляемого запроса:

import axios from "axios";
import {AUTH} from "../baseURLs"


const login =  (email, password) => {
   try {
       return axios.post(`http://localhost:3000/api${AUTH}/login`, {email, password})
   } catch(err) {
       console.log('API error', err)
   }
}

Теперь мы видим, что мы импортировали константу имени controller и добавили ее в route. Теперь, если мы хотим добавить API регистрации, мы можем просто передать константу AUTH в также маршрут:

import axios from "axios";
import {AUTH} from "../baseURLs"


const login =  (email, password) => {
   try {
       return axios.post(`http://localhost:3000/api${AUTH}/login`, {email, password})
   } catch(err) {
       console.log('API error', err)
   }
}


const signUp = (email, password) => {
   try {
       return axios.post(`http://localhost:3000/api${AUTH}/signup`, {email, password})
   } catch(err) {
       console.log('API error', err)
   }
}

И теперь, как мы видим, централизовав имена контроллеров в отдельном файле, мы избегаем дублирования и упрощаем внесение изменений в будущее. Таким образом, нам нужно только изменить имя контроллера в одном месте, и все использующие его построители конечных точек получат обновленное значение. Это может сэкономить нам много времени и усилий при поддержке кодовой базы.

Но у нас по-прежнему есть дубликат базового URL-адреса сервера, то есть «localhost:3000/api» в наш случай. Чтобы решить эту проблему, решение будет зависеть от используемого подхода. Если используется обычная функция fetch, базовый URL можно добавить в файл baseURLs<. /em>, а затем его можно импортировать при необходимости. Для тех, кто использует Axios, популярную среди разработчиков React, можно создать экземпляр Axios для хранения базового URL. внутреннего сервера:

import axios from 'axios';

const instance = axios.create({
  baseURL: 'http://localhost:3000/api',
});

export default instance;

Чтобы реализовать это, создайте новый файл с именем «http» и добавьте в него приведенный выше код. Затем мы можем импортировать экземпляр везде, где это необходимо.

Как показано, мы создали экземпляр axios экземпляр, который может совместно использоваться всеми построителями конечных точек, чтобы использовать тот же базовый URL. Преимущество здесь в том, что мы также можем добавить rперехватчики запросов и ответов, а также заголовки в наш запрос и другие интересные функции. Однако наша главная цель — унифицировать базовый URL, чтобы каждый построитель конечных точек использовал один и тот же экземпляр axios.

Теперь мы можем внести небольшое изменение в наши файлы построителя конечных точек, чтобы импортировать этот экземпляр:

import {AUTH} from "../baseURLs"
import http from "../http"


const login =  (email, password) => {
   try {
       return http.post(`${AUTH}/login`, {email, password})
   } catch(err) {
       console.log('API error', err)
   }
}

const signUp = (email, password) => {
   try {
       return http.post(`${AUTH}/signup`, {email, password})
   } catch(err) {
       console.log('API error', err)
   }
}

Как видите, мы изменили способ использования axios нашими конструкторами конечных точек. Мы импортировали только что созданный axios экземпляр и назвали его «http», чтобы отличить его от обычной axios библиотеки. Кроме того, вы можете видеть, что мы удалили добавление базового URL внутри маршрута, поскольку он уже добавлен в экземпляр.

И теперь мы можем сказать, что решили проблему дублирования для baseURL и имен контроллеров.

Однако в определенных ситуациях мы можем столкнуться с проблемами читаемости, особенно при отправке параметров или параметров запроса в серверную часть. Это связано с тем, что теперь у нас есть две части логики внутри построителей конечных точек. Первая часть включает в себя создание конечной точки, которую мы уже знаем. Вторая часть включает в себя построение самого маршрута, потому что маршрут теперь нужно создавать динамически на основе полученных параметров или параметров запроса, таких как параметры ID или keyword.

const getUserDetails = (userId) => {
   try {
       return http.get(`api${USERS}/${userId}`)
   } catch(err) {
       console.log('API error', err)
   }
}

Если мы посмотрим на приведенный выше код, то увидим, что теперь у нас есть еще одна переменная, которую необходимо включить в маршрут, а именно userId.

И это может привести к путанице между константами baseURL и допустимыми переменными в некоторых сложных случаях.

Чтобы решить эту проблему, мы можем использовать построители маршрутов. Но что именно они собой представляют? По сути, построители маршрутов представляют собой функции, которые объединяют строки и переменные. , создавая маршрут API, готовый к использованию. В нашем сценарии мы можем включить новый построитель маршрутов, который объединяет имя контроллера и идентификатор пользователя для более раннего API. Затем мы можем импортировать этот построитель в файл построитель конечных точек и использовать его в функции axios для получения route URL строка, которая будет готова к использованию.

Но прежде чем мы продолжим, нам нужно внести еще одно изменение в структуру файла, чтобы еще больше ее организовать. Нам нужно создать новый каталог под названием «маршруты», который будет иметь ту же структуру, что и «конечные точки. », как показано ниже:

Тем не менее содержимое этих файлов будет отличаться, поскольку они будут содержать построители маршрутов. Если мы более внимательно изучим файл пользователей в каталоге маршрутов, мы обнаружим код, похожий на следующий:

import {USERS} from "../baseURLs"


export const getUserDetailsRoute = (userId) => {
   return `${USERS}/${userId}`
};

Как видите, getUserDetailsRoute – это функция построения маршрута, которая объединяет userID с импортированным имя контроллера, в нашем случае это контроллер USERS. Это простая функция, но она подразумевает, что построитель конечных точек теперь отвечает только за создание функции запроса HTTP.

Мы можем импортировать построитель маршрутов в файл конечной точки и заменить жестко заданную строку в функции axios на этот построитель маршрутов. и вызовите ее:

import {getUserDetailsRoute} from "../routes/users"

const getUserDetails = (userId) => {
   try {
       return http.get(getUserDetailsRoute(userId))
   } catch(err) {
       console.log('API error', err)
   }
}

Как видите, теперь мы можем просто вызвать функцию построитель маршрутов и передать полученный userId, а он сгенерирует строку маршрута для нас. С этого момента нам нужно сосредоточиться только на создании API и потенциальном извлечении данных из него.

Используя описанный выше подход, мы можем гарантировать, что наши конечные точки построены организованно и эффективно. Теперь мы можем справиться с проблемой наличия разных контроллеров и API. Однако мы можем столкнуться с другой проблемой, когда некоторые компоненты используют разные API от разных контроллеров. В таком случае мы можем потерять ориентацию из-за многочисленных операторов импорта. Кроме того, если мы хотим внести глобальные изменения в построители конечных точек, например извлечь объект данных из ответа, нам потребуется обновлять каждый построитель конечных точек отдельно.

Чтобы решить эту проблему, мы должны рассмотреть возможность создания контейнера, содержащего все конечные точки, чтобы мы могли импортировать его один раз и получить доступ к любому >конечная точка, которую нам нужно использовать. Кроме того, если мы хотим применить глобальное изменение, мы можем добавить это изменение в контейнер. Для этого мы можем использовать React Hooks.

Ловушка, которую мы хотим создать, называется «useEndpoints». Этот хук по существу сохраняет все конечные точки в нашей системе и экспортирует их как один объект. Всякий раз, когда нам нужно использовать API, мы можем импортировать этот хук и легко получить доступ ко всем конечным точкам, определенным в нашей системе:

import authEndpoints from "../../API/endpoints/authentication";
import userEndpoints from "../../API/endpoints/users";
import productEndpoints from "../../API/endpoints/products";


const useEndpoints = () => {
   const collectedEndpoints = {
       ...authEndpoints,
       ...userEndpoints,
       ...productEndpoints
   };
   return collectedEndpoints;
};




export default useEndpoints;

Как мы видим, все конечные точки теперь содержатся в одном контейнере. Мы можем импортировать этот хук в наш компонент и легко извлечь из него нужную конечную точку:

import useEndpoints from "../../hooks/useEndpoints";


const LoginForm = () => {
   const {login} = useEndpoints();


   const handleSubmit = (e) => {
       e.preventDefault();
       login(email, password)
           .then(res => {
               console.log(res)
           })
           .catch(err => {
               console.log(err)
           })
   }


   return (
       <form onSubmit={handleSubmit}>
           <input type="email" placeholder="email" />
           <input type="password" placeholder="password" />
           <button type="submit">Login</button>
       </form>
   )
}


export default LoginForm;

Как показано в приведенном выше примере, компонент loginForm теперь может просто вызывать логин конечную точку и вся необходимая функциональность будет обслуживаться конечной точкой.

Теперь предположим, что мы хотим извлечь свойство данных из объекта response. Мы можем легко добавить это изменение в хук useEndpoints, и оно будет применено ко всем конечным точкам:

import authEndpoints from "../../API/endpoints/authentication";
import userEndpoints from "../../API/endpoints/users";
import productEndpoints from "../../API/endpoints/products";


const useEndpoints = () => {
   const collectedEndpoints = {
       ...authEndpoints,
       ...userEndpoints,
       ...productEndpoints
   };


   Object.keys(collectedEndpoints).forEach(key => {
       const originalEndpoint = collectedEndpoints[key];


       collectedEndpoints[key] = async (...args) => {
           const {data} = await originalEndpoint(...args);
           return data;
       }
   })




   return collectedEndpoints;
};




export default useEndpoints;

В приведенном выше коде показано, как мы перебирали ключи собранных конечных точек объекта, которые являются действительными конечными точками. Мы назначили новую переменную для каждой итерации для хранения исходной конечной точки, а затем изменили ключ с помощью нового асинхронная функция, которая вызывает originalEndpoint и извлекает данные из него и возвращает данные. После изменений все собранные конечные точки теперь будут представлены как новые асинхронные функции которые вызывают исходные конечные точки, а также извлекают данные из ответа.

При таком подходе, даже если в вашей системе 200 конечных точек, внесение глобальных изменений, таких как извлечение данных из ответа, становится простым и без головной боли.

Заключение

Теперь, когда мы увидели, как создать структуру папок, изменить ее в соответствии с новыми требованиями, создать построители конечных точек для создания функций для выполнения HTTP-запросов, добавить базовые URL-адреса для имен контроллеров, создать глобальный экземпляр Axios, создать построители маршрутов. для объединения переменных с именами контроллеров для создания готовых к использованию маршрутов, а также создания и изменения хука useEndpoints, мы можем видеть, насколько мощна эта структура для организации нашей кодовой базы и улучшения нашего понимания кода из динамическая перспектива. Мы можем многое узнать из этого кода и применить его к другим случаям, которые требуют глобальных изменений или организации файлов.

Теперь мы можем показать изображение, которое демонстрирует все, что мы сделали с архитектурной точки зрения:

Я надеюсь, что вы нашли эту статью полезной и сможете применить эти принципы в своей собственной кодовой базе, чтобы ощутить волшебство использования API-интерфейсов бэкэнд-сервера.

Социальное:

Линкедин

"Веб-сайт"