Архитектура современного 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 разработчиками.

Парадигмы программирования - императивная и декларативная

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

В этой статье поговорим о парадигмах программирования. Затронем императивную и декларативную парадигмы. Для сравнения разберем несколько небольших примеров. В конце мы взглянем на парадигмы с точки зрения эволюции.

Это серия статей о функциональном программировании:
  1. Парадигмы программирования (рассматривается в этой статье)
  2. Композиция
  3. Функторы
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Парадигмы программирования

Парадигма программирования — это стиль или "способ" программирования. Поэтому некоторые языки заставляют нас писать в определенной парадигме. Другие языки оставляют варианты открытыми для программиста, где каждая парадигма следует набору понятий. За всю историю компьютерного программирования инженеры разработали разные языки. Каждый язык основывался на одной или нескольких парадигмах. Эти парадигмы принадлежат к одной из следующих двух основных категорий:

1. Императивная парадигма

В императивных языках программирования поток управления является явным, где программист инструктирует программу, как изменить ее состояние. В императивную парадигму также включается:
  • Структурная парадигма
  • Объектно-ориентированная парадигма

2. Декларативная парадигма

В декларативной парадигме поток управления является неявным, когда программист указывает программе, что следует делать, не указывая, как это должно быть сделано. В декларативную парадигму также включается:
  • Функциональная парадигма
  • Логическая парадигма
  • Математическая парадигма
  • Реактивная парадигма
Большинство языков принадлежат либо к императивной, либо к декларативной парадигме, где каждая парадигма имеет набор понятий, которым необходимо следовать. Рассмотрим подробнее каждую парадигму.

Императивная парадигма

Императивная парадигма немного изменилась из-за структурной парадигмы, но у нее все еще есть проблемы:
  • Указание программе, как что-то делать (поток управления является явным)
  • Общее состояние
Чтобы понять эти проблемы рассмотрим примеры.

Проблема 1: Указание программе, как что-то делать (поток управления является явным)

Кейс: представьте себе 1000 сотрудников с руководителем, который ведет их по проекту. Руководитель начинает рассказывать 1000 сотрудников, как делать вещи одну за другой. Как вы думаете, насколько это будет плохо? Я почти уверен, что вы видите, что этот стиль управления на микроуровне имеет большие риски, ловушки и даже не сработает. Решение: Сгруппировать людей по зонам ответственности и делегировать в каждую группу руководителя группы. Руководитель каждой группы должен знать, как делать что-то для достижении цели. Это значительно уменьшит сложность, узкие места и станет намного проще в управлении. В этой аналогии
  • Руководитель = Программист
  • Руководители групп = Функции более высокого уровня
  • Сотрудники в каждой группе = Строки кода
Вывод: когда мы применяем организационную структуру более высокого порядка на программном уровне, наша жизнь становится проще.

Проблема 2: Общее состояние

Кейс: Представьте отца, у которого двое детей. У них есть общий банковский счет. Каждый месяц отец кладет на этот счет 1000 долларов. Оба ребенка не знают, что учетная запись используется совместно. Таким образом, они оба думают, что у каждого есть 1000 долларов, которые он может потратить на себя. В конце месяца оказывается, что на этом счету осталось -1000 долларов. Решение: У каждого ребенка должна быть отдельная учетная запись и указанная ежемесячная сумма. В этой аналогии:
  • Дети = Функции
  • Общий банковский счет = общее состояние
Вывод: Когда ваши функции имеют одно и то же состояние, они используют его неосознанно. Это испортит состояние вашей программы даже с двумя функциями. Так что всегда лучше, чтобы каждая функция имела собственное независимое состояние для использования.

Пример императивной парадигмы

Давайте посмотрим, как функция для суммирования может быть реализована в императивной парадигме.
const sum = (list) => {
  let result = 0
  for (let i = 0; i < list.length; i++) {
    result += list[i]
  }
  return result
}
Почему этот код считается императивным?
  1. Указание программе, как что-то делать (поток управления является явным): мы явно сообщаем циклу for, как работать. Также мы обращаемся к каждому элементу в массиве явно.
  2. Совместное состояние: результирующая переменная является общим состоянием, изменяющимся на каждой итерации (с общим состоянием в более крупных решениях будет гораздо сложнее справиться).

Декларативная парадигма

Декларативная парадигма — это когда программист указывает программе, что должно быть сделано, не указывая, как. В декларативной парадигме мы пишем функции, которые:
  • Описывают, что должна выполнять программа, а не как (неявный поток управления).
  • Не производят побочных эффектов (о которых мы поговорим позже).

Пример декларативной парадигмы

Мы увидели, как функция sum может быть реализована в императивной парадигме. Давайте посмотрим, как ее можно реализовать декларативно.
const add = (a, b) => a + b
const sum = (list) => list.reduce(add)
Похоже на магию? Но почему это считается декларативным?
  • Описано, что программа должна выполнять, а не как (неявный поток управления): нет явного итератора, нет явного указания циклу, как работать или как получить доступ к элементам. Это было достигнуто с помощью метода reduce.
  • Не производит побочных эффектов: общее состояние — это форма побочных эффектов, которая была полностью устранена с помощью метода reduce и функции add.

Еще одно сравнение

Что, если мы хотим суммировать только четные числа? Разберем эту задачу на примерах в разных парадигмах.

Императивная реализация

const evenSum = (list) => {
  let result = 0
  for (let i = 0; i < list.length; i++){
    if(list[i] % 2 === 0) {
      result += list[i]
    }
  }
  return result
}

Декларативная реализация

const evenSum = (list) => {    
  const isEven = (n) => n % 2
  const add = (a, b) => a + b
  return list.filter(isEven).reduce(add)
}
Как видим, если мы хотим сравнить обе парадигмы (императивную и декларативную), то декларативная парадигма (в нашем случае Функциональная парадигма) больше похожа на шестеренки. Вы разрабатываете свои шестеренки как отдельные единицы, затем добавляете их туда, где они вам нужны. Но в императивной парадигме это больше похоже на тесто. Почти все смешано и слито в один и тот же кусок код. В целом декларативная парадигма — это:
  • Предсказуемость
  • Тестируемость
  • Многоразовость
  • Настраиваемость
  • Кэшируемость
  • Поддерживаемость
  • Компонуемость
Некоторые из этих моментов не обязательно имеют смысл в контексте примера с функцией sum, но будут иметь смысл в следующих статьях о функциональном программировании.

Эволюция парадигм

Итак, у нас есть 2 основные парадигмы: императивная и декларативная, каждая из которых имеет подпарадигмы. Теперь поговорим подробнее о структурной, объектно-ориентированной и функциональной парадигмах. с эволюционной точки зрения. Каждая парадигма ограничивала способ программирования, вводя что-то новое.
  • Структурная парадигма: ограниченное использование goto и «потока передачи управления» за счет введения в наш код такой структуры, как if/else/then/loop и других. Другими словами, он ограничивает поток передачи управления.
  • Объектно-ориентированная парадигма: ограничение полиморфизма с использованием указателей на функции за счет введения полиморфизма с использованием наследования.
  • Функциональная парадигма: ограничения общего состояния и побочные эффекты за счет введения иммутабельности.
Имейте в виду, что каждая парадигма может использовать одну или несколько концепций других парадигм (например, как объектно-ориентированная, так и функциональная парадигмы используют концепции структурной парадигмы).

Итоги

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