Многоуровневая архитектура React компонентов

год назад·4 мин. на чтение

Раскрываем секреты создания масштабируемых и поддерживаемых React компонентов

Сопровождаемость является важным аспектом любой системы. Он определяет, насколько легко система может быть обновлена. Система будет работать оптимально только в том случае, если все компоненты хорошо обслуживаются. Если ваш проект имеет хорошо поддерживаемую архитектуру, разработчики могут легко понять проект и внести точечные изменения, чтобы повысить производительность при одновременном сокращении циклов разработки, тестирования и выпуска. Архитектура проекта является ключевым фактором, определяющим простоту обслуживания компонентов проекта. Многоуровневая архитектура является одной из лучших архитектур для написания поддерживаемых компонентов для UI фреймворков, таких как React. Поэтому в этой статье мы обсудим, как использовать многоуровневую архитектуру для написания простых в обслуживании компонентов на React и каких ошибок следует избегать.

Что такое многоуровневая архитектура и зачем ее использовать?

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

Трехуровневая архитектура

Вот эти три основных слоя: Презентационный слой (уровень представления, presentation layer) Уровень бизнес-логики (business logic layer, BLL) Уровень доступа к данным (data access layer, DAL) Уровень представления управляет взаимодействием с пользователем и представляет данные пользователю. Этот слой может представлять собой форму веб-интерфейса, десктопного или мобильного приложения. Он взаимодействует с уровнем бизнес-логики для выполнения действий и извлечения данных. Уровень бизнес-логики отвечает за реализацию бизнес-правил и рабочих процессов приложения. Этот уровень должен быть полностью независимым от уровня представления и не иметь представления о пользовательском интерфейсе. Он должен содержать всю логику и алгоритмы приложения и взаимодействовать с уровнем доступа к данным для получения необходимых данных. Уровень доступа к данным отвечает за взаимодействие с источниками данных приложения. Этот уровень отвечает за извлечение и хранение данных и должен быть отделен от уровня бизнес-логики. Он должен включать весь код, связанный с доступом к базе данных, вызовами API и другими внешними источниками данных.

Как использовать трехуровневую архитектуру в React

Давайте теперь рассмотрим, как мы можем применить принцип многоуровневой архитектуры к нашим React приложениям.

Уровень доступа к данным

Этот слой обычно реализуется в виде набора служебных функций (утилит), которые могут быть повторно использованы различными кастомными хуками. Рассмотрим следующую служебную функцию fetchData(), которая используется для получения данных из API.
export default async function fetchData() {
  const response = fetch('https://jsonplaceholder.typicode.com/users/1').then(
    (res) => {
      if (res.ok) return res.json();
      return Promise.reject(res);
    }
  );
  return response;
}
Эту функцию можно использовать на уровне бизнес-логики всякий раз, когда вам нужно получить данные API без написания дублирующегося кода. Вы можете передавать URL-адрес через аргумент и изменять функцию по мере роста проекта. При тестировании вызовов API вы можете вызвать эту функцию с фиктивными данными, чтобы упростить процесс.

Уровень бизнес-логики

Этот слой обычно реализуется в виде набора пользовательских хуков, которые можно повторно использовать в разных компонентах. Рассмотрим следующий пользовательский хук seUserData(), который используется для получения данных о пользователе.
import React from "react";
import fetchData from "../util/fetchData";

const useUserData = () => {
  const [userData, setUserData] = useState([]);
  useEffect(() => {
    fetchData()
      .then((data) => setUserData(data))
      .catch((res) => console.error(res.status));
  }, []);
  return [userData];
};

export default useUserData;
Как видите, здесь вызывается функция fetchData() из слоя доступа к данным. Чтобы сделать этот хук более переиспользуемым, передайте путь URL-адреса в качестве аргумента кастомному хуку и передайте его в функцию fetchData().

Презентационный слой

Уровень представления должен содержать все компоненты пользовательского интерфейса, отвечающие за отрисовку данных и реагирование на действия пользователя в приложении React. В этих компонентах не должно быть бизнес-логики или кода запроса данных. Взглянем на пример компонента UserProfile ниже.
import React from "react";
import useUserData from "./customHooks/useUserData";

const UserProfile = () => {
  const [userData] = useUserData();
  return (
    <div>
      {userData.id ? (
        <div>
          <ul> {userData.name} </ul>
          <ul> {userData.email} </ul>
        </div>
      ) : (
        <p>Loading data...</p>
      )}
    </div>
  );
}

export default UserProfile;
Пользовательский хук useUserData() используется в компоненте для взаимодействия с уровнем бизнес-логики и получения пользовательских данных для отображения пользователю. Помимо функции return, единственное, что должно быть включено в UI компоненты, как показано в этом примере, — это вызовы функций для уровня бизнес-логики. Это дает вам четкие UI компоненты, что значительно упрощает поиск и исправление ошибок, связанных с пользовательским интерфейсом.

Ошибки, которых следует избегать при использовании многоуровневой архитектуры

  • Переусложнение — поддерживайте простую и масштабируемую архитектуру и отказывайтесь от использования подходов, которые не соответствуют шаблонам React, таких как подходы на основе наследования.
  • Сильная связанность — когда слои плотно связаны, то заменить один, не затрагивая другие, может быть стать сложной задачей. Уменьшите связанность, используя соответствующие шаблоны и методы, такие как внедрение зависимостей и интерфейсы.
  • Игнорирование производительности — при неправильной реализации многоуровневая архитектура может повлиять на производительность приложения. При оптимизации архитектуры для повышения производительности учитывайте такие факторы, как разделение кода, отложенная загрузка и кэширование.
  • Плохие соглашения об именах — используйте четкие и согласованные соглашения об именах для слоев, компонентов и функций. В противном случае разработчикам будет сложно понять и поддерживать код в долгосрочной перспективе.
  • Отсутствие тестирования — каждый слой должен быть тщательно протестирован, чтобы убедиться, что он работает должным образом. Неспособность протестировать каждый слой может привести к ошибкам и другим проблемам в приложении.
  • Отсутствие связности — каждый слой должен иметь высокую степень связности. Связность относится к тому, насколько тесно связаны функции и обязанности внутри слоя. Низкая связность может привести к тому, что код будет трудно понять и поддерживать.

Итоги

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

Архитектура современного React приложения

2 года назад·6 мин. на чтение

Есть одна проблема, с которой сталкивается каждый React-разработчик на своем пути. Это то, как построить хорошую архитектуру приложения, какая структура приложения будет идеальной.

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

Компоненты

Что в первую очередь создает React-разработчик в проекте? React-приложения создаются с помощью компонентов. Существует много различных архитектур (некоторые очень хорошие, а другие ужасные) и есть весьма универсальный путь, который можно использовать в большинстве случаев, даже для небольших проектов. Вот как это выглядит:
├── components
│   ├── common
│   │   └── button
│   │       ├── button.tsx
│   │       ├── button.stories.tsx
│   │       ├── button.spec.tsx
│   │       └── index.ts
│   └── signup-form
│       ├── signup-form.tsx
│       ├── signup-form.spec.tsx
│       └── index.ts
Ключевым моментом здесь является следующее: у нас есть папка components, содержащая все компоненты, которые используются в приложении более одного раза, поэтому мы собираемся исключить все специфические компоненты из этой папки. Почему? Просто потому, что смысл этой папки заключается в том, чтобы содержать логику многократного использования. Кнопка должна использоваться почти на каждой странице нашего приложения, поэтому и существует общая папка common. Для компонента signup-form происходит нечто иное. Почему? Предположим, что у нас есть две разные страницы (подробнее об этом позже) для входа и регистрации, компонент signup-form должен повторяться два раза, вот почему он помещен в папку components.
Обратите внимание, что это исключительный случай, если бы у нас была одна страница для аутентификации, нам не следовало бы помещать его в components. Вы, наверное, также заметили, что каждый компонент помещен в соответствующую директорию с очень простым для понимания соглашением об именовании.
button
├── button.tsx
├── button.stories.tsx
├── button.spec.tsx
└── index.ts
Это потому, что ваше приложение в конечном итоге может содержать более 1000 компонентов, и, если все они имеют тесты или файл storybook, это может легко превратиться в беспорядок. Давайте рассмотрим некоторые ключевые моменты этой папки:
  • Все файлы, связанные с этим компонентом, находятся в этой папке.
  • Все экспортируемые модули помещаются в index.ts, чтобы избежать двойного имени в импорте.
  • Все файлы названы в kebab-case.
Это может казаться немного многословным, особенно для новичков или для маленьких проектов, но это требует очень мало усилий, а в качестве ответной меры - выигрыш в читабельности кода. Вот пример. Попробуйте ответить на эти вопросы:
  • Где находится компонент кнопки? -> В папке button.
  • Где находятся сторибуки для этой кнопки? -> В папке button.
  • Мне нужно найти тесты для этой кнопки, где я могу его найти? -> Очевидно, в папке button.
Еще раз, если вы считаете эти вопросы глупыми и очевидными, придет день, когда вы будете работать над кодовой базой, где лучшие практики - последнее, о чем думали, и вы вспомните эту статью. Мы еще не закончили с компонентами, но вернемся к этому позже.

Страницы

Отдельной сущности для страниц в React не существует. Они тоже являются компонентами, состоящими из других компонентов. Но в отличие от других компонентов, обычно они очень строго привязаны (например, в определенный путь URL). Куда же их вставлять? Мы можем использовать каталог views (или pages, если хотите), в который помещаются все эти вещи, например:
views
├── home.tsx
├── guestbook.tsx
└── newsletter
   ├── index.ts
   ├── newsletter.tsx
   └── components
       └── newsletter-form
           ├── newsletter-form.tsx
           ├── newsletter-form.spec.tsx
           └── index.ts
Для home и guestbook все довольно просто, страница должна быть результатом композиции других компонентов, которые имеют соответствующие тесты, поэтому для них нет специального каталога.
Иначе обстоит дело со страницей newsletter , на которой есть нечто специфическое, компонент newsletter-form. В этом случае используется подход создания вложенной папки компонентов внутри папки страницы и действуем так же, как в обычной папке компонентов, используя те же правила. Этот подход очень эффективен, поскольку позволяет разделить код на небольшие фрагменты, но при этом сохраняет хорошо организованную архитектуру. Компонент newsletter-form не следует помещать в папку с общими components, просто потому, что это единственное место, в котором он используется. Если приложение будет расти, и компонент будет использоваться в нескольких частях, ничто не помешает вам переместить его. Еще один совет - сохранять согласованное имя между страницей и маршрутом, примерно так:
<Route path="/bookings">
 <Route index element={<Bookings />} />
 <Route path="create" element={<CreateBooking />} />
 <Route path=":id" element={<ViewBooking />} />
 <Route path=":id/edit" element={<EditBooking />} />
 <Route path=":id/delete" element={<DeleteBooking />} />
</Route>

Лэйауты (Layouts, Макеты)

Лэйауты вообще не являются страницами, они больше похожи на компоненты, поэтому с ними можно обращаться так же, но лучше помещать их в папку layouts, так понятнее, что в этом приложении есть n лэйаутов.
layouts
├── main.tsx
└── auth.tsx
Вы можете заметить, что мы не называем их main-layout.tsx, а просто main.tsx, потому что, следуя этому шаблону, нам пришлось бы переименовать все компоненты, например, table-component.tsx, что странно. Поэтому называем все компоненты без очевидного суффикса, задаваемого родительским каталогом, а если нужно подчеркнуть, что используется макет, всегда можно использовать псевдоним импорта.
import { Main as MainLayout } from "@/layouts/main.tsx";

Контексты, хуки и хранилища

Это довольно просто, и обычно, почти все разработчики придерживаются чего-то подобного:
hooks
├── use-users.ts
└── use-click-outside.ts
contexts
├── workbench.tsx
└── authentication.tsx
Опять же, для единообразия используется kebab-case для всех имен файлов, так что нам нужно беспокоиться о том, какие из них написаны заглавными буквами, а какие нет. Для тестовых файлов, из-за того, что пользовательских хуков немного, не обязательно создавать отдельную папку, но, если вы хотите быть очень строгими, вы можете сделать и это:
hooks
├── use-users
│   ├── use-users.ts
│   ├── use-users.spec.ts
│   └── index.ts
└── use-click-outside.ts

Функции-помощники (хэлперы, helpers)

Сколько раз вы создавали красивую функцию formatCurrency, не зная, куда ее положить? Папка helpers придет вам на помощь. Обычно сюда помещаются все файлы, которые используются для того, чтобы код выглядел лучше. Не важно, будет ли функция использоваться несколько раз или нет.
helpers
├── format-currency.ts
├── uc-first.ts
└── pluck.ts

Константы

Существует много проектов, которые содержат константы в папке utils или helpers, но лучше помещать их в отдельный файл, давая пользователю хороший обзор того, что используется в качестве константы в приложении. Чаще всего в эту папку помещаются только глобальные константы, поэтому не стоит помещать сюда константу QUERY_LIMIT, если она используется только в одной функции для очень специфического случая.
constants
└── index.ts
Кроме того, можно хранить все константы в одном файле. Нет смысла разбивать каждую константу на отдельные файлы.
// @/constants/index.ts
export const COMPLANY_EMAIL = "example@example.com";
И использоваться они будут так:
import { COMPLANY_EMAIL } from "@/constants";

Стили

Просто поместите глобальные стили в папку styles, и все готово.
styles
├── index.css
├── colors.css
└── typography.css
А как насчет CSS для компонентов? Хороший вопрос. Помните папку компонентов, о которой мы говорили некоторое время назад? Так вот, вы можете добавить больше файлов в зависимости от ваших потребностей.
button
├── button.tsx
├── button.stories.tsx
├── button.styled.tsx
├── button.module.scss
├── button.spec.tsx
└── index.ts
Если вы используете emotion, styled-components или просто CSS Modules, поместите их в папку конкретного компонента, чтобы все было оптимально упаковано.

Конфигурационные файлы

Есть ли у вашего приложения файлы конфигурации, такие как Dockerfiles, Fargate Task Definitions, webpack и т.д.? Папка config должна быть идеальным местом для них. Помещение их в соответствующую директорию позволяет избежать загрязнения корневого каталога не относящимися к делу файлами.

API

99% приложений react имеют хотя бы один вызов API к внешнему источнику данных (вашему бэкенду или какому-то публичному сервису), обычно эти операции выполняются в нескольких строках кода без особых сложностей, и именно поэтому, оптимальная их организация игнорируется. Рассмотрим этот код:
axios
 .get("https://api.service.com/bookings")
 .then((res) => setBookings(res.data))
 .catch((err) => setError(err.message));
Довольно просто, верно? Теперь представьте, что у вас есть эти 3 строки, распределенные по 10 компонентам, потому что вы часто используете именно этот адрес сервера. Надеюсь, вы не хотите выполнять поиск и замену всех URL в приложении, кроме того, если вы используете TypeScript, импортировать каждый раз тип ответа - это довольно повторяющееся действие.
Вместо этого рассмотрите возможность использования каталога api, который, прежде всего, содержит экземпляр клиента, используемого для вызовов, например, fetch или axios, а также файлы, содержащие декларации вызовов fetch.
api
├── client.ts
├── users.ts
└── bookings.ts
И пример файла users.ts:
export type User = {
 id: string;
 firstName: string;
 lastName: string;
 email: string;
};

export const fetchUsers = () => {
 return client.get<User[]>("/users", {
   baseURL: "https://api.service.com/v3/",
 });
};

Итоги

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