Есть одна проблема, с которой сталкивается каждый React-разработчик на своем пути. Это то, как построить хорошую архитектуру приложения.
Эта статья поможет вам избежать некоторых распространенных ошибок, которые большинство из нас допускают при создании архитектуры приложений на реакте, и подскажет вам правильный способ структурирования каталогов.
Это для вас?
Прежде чем начать, необходимо подчеркнуть один момент: не существует идеального решения, которое подходит для любого возможного случая. Это особенно важно понимать, потому что многие разработчики всегда ищут единственное и неповторимое решение всех своих проблем, и мне жаль говорить, что если вы ищете его, то эта статья не для вас.
Время архитекторов!
Если вы попали сюда, это значит, что вы заинтересовались этой темой, так что, наконец, пора начинать! Все содержимое, которое я буду упоминать, будет помещено в каталог src
, и каждое упоминание новых папок будет относиться к этому ограничению, имейте это в виду.
Компоненты
Что в первую очередь создает React Developer в проекте? Я бы сказал, компоненты, потому что, как вы знаете, 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
. Для компонента формы регистрации происходит нечто иное, почему она является многоразовой?
Предположим, что у нас есть две разные страницы (подробнее об этом позже) для входа и регистрации, этот компонент нужно повторить два раза, вот почему он помещен в папку components, но как scoped logic
.
Обратите внимание, что, как я уже говорил, это конкретный случай, если бы у нас была одна страница для аутентификации, нам не следовало бы помещать его сюда.
Некоторые примеры того, что можно вставить в папку common
:
- Входы
- Карточки
- Оповещения
Думаю, вы уловили суть.
Вероятно, вы также заметили, что каждый компонент помещен в соответствующую директорию с очень простым для понимания соглашением об именовании.
button
├── button.tsx
├── button.stories.tsx
├── button.spec.tsx
└── index.ts
Это потому, что ваше приложение может содержать более 1000 компонентов, и если у каждого из них есть тест или файл storybook, это может легко привести к беспорядку. Давайте рассмотрим некоторые ключевые моменты этой папки:
- Все файлы, связанные с компонентами, находятся в этой папке.
- Все экспортируемые модули помещаются в index.ts, чтобы избежать ужасного двойного имени в импорте.
- Все файлы названы в кебабном регистре.
Я знаю, что это кажется немного многословным, особенно для новичков или для маленьких проектов, но это требует очень мало усилий, а в качестве ответной меры — выигрыш в читабельности кода, хотите пример? Попробуйте ответить на эти вопросы:
- Где находится компонент кнопки? -> В папке с кнопками.
- Где находятся истории для этой кнопки? -> В папке 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
не следует помещать в папку «main» 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>
Макеты
Макеты вообще не являются страницами, они больше похожи на компоненты, поэтому с ними можно обращаться так же, но в последнее время я предпочитаю помещать их в папку layout
, так понятнее, что в этом приложении доступно n макетов.
layout
├── main.tsx
└── auth.tsx
Вы можете заметить, что я не называю их main-layout.tsx
, а просто main
, потому что, следуя этой причине, мне пришлось бы переименовать все компоненты, например table-component.tsx
, что странно. Поэтому я называю все компоненты без очевидного суффикса, задаваемого родительским каталогом, а если мне нужно подчеркнуть, что я использую макет, я всегда могу использовать псевдоним импорта.
import { Main as MainLayout } from "@/layouts/main.tsx";
Контексты, крючки и хранилища
Это довольно просто, и обычно я вижу, что почти каждый разработчик придерживается чего-то подобного, поэтому я собираюсь описать здесь, как я организую эти вещи:
hooks
├── use-users.ts
└── use-click-outside.ts
contexts
├── workbench.tsx
└── authentication.tsx
Здесь я снова придерживаюсь использования кебабного регистра для всех имен файлов, так что мне не нужно беспокоиться о том, какие из них написаны с заглавной буквы, а какие нет. Для тестовых файлов, из-за того, что пользовательских хуков немного, я бы не стал создавать отдельную папку, но, на мой взгляд, если вы хотите быть очень строгими, вы можете сделать и это:
hooks
├── use-users
│ ├── use-users.ts
│ ├── use-users.spec.ts
│ └── index.ts
└── use-click-outside.ts
Помощники
Сколько раз вы создавали красивую функцию formatCurrency
, не зная, куда ее положить? Папка helpers
придет вам на помощь!
Обычно сюда я помещаю все файлы, которые я использую для того, чтобы код выглядел лучше, мне не важно, используется ли функция несколько раз или нет. Обычно этих хелперов довольно мало, поэтому пока их не очень много, я придерживаюсь этого пути.
helpers
├── format-currency.ts
├── uc-first.ts
└── pluck.ts
Константы
Я вижу много проектов, которые содержат константы в папке utils
или helpers
, я предпочитаю помещать их в отдельный файл, давая пользователю хороший вид того, что используется как константа в приложении. Чаще всего я помещаю только глобальные константы, поэтому не стоит помещать сюда константу QUERY_LIMIT
, если она используется только в одной функции для очень специфического случая.
constants
└── index.ts
Кроме того, я обычно храню все константы в одном файле. Нет смысла разбивать каждую константу на отдельные файлы.
// @/constants/index.ts
export const LINKEDIN_FULLNAME = "Renato Pozzi";
export const TWITTER_USERNAME = "@itsrennyman";
// And use them in your app! 👍
import { LINKEDIN_FULLNAME, TWITTER_USERNAME } 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 и так далее? Папка 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.
Вы также используете одну или несколько из этих техник в своих проектах? Дайте мне знать через Twitter или LinkedIn!