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

Импорт и экспорт React компонентов

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

Как импортировать и экспортировать React компоненты

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

Файл корневого компонента

В части "Пишем первый React компонент" мы создали компонент Profile и компонент Gallery , который его отображает:
function Profile() {
  return <img src="https://example.com/userpic.jpg" alt="User Name" />;
}

export default function Gallery() {
  return (
    <section>
      <h1>Amazing scientists</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}
В настоящее время они находятся в корневом файле компонента, который в этом примере называется App.js. В Create React App ваше приложение находится в src/App.js. Однако в зависимости от вашей настройки ваш корневой компонент может находиться в другом файле. Если вы используете фреймворк с файловой маршрутизацией, например Next.js, ваш корневой компонент будет разным для каждой страницы.

Экспорт и импорт компонента

Что, если в будущем вы захотите изменить посадочную страницу и разместить там список научных книг? Или разместить все профили в другом месте? Имеет смысл переместить Gallery и Profile из корневого файла компонента. Это сделает их более модульными и позволит повторно использовать их в других файлах. Вы можете переместить компонент в три шага:
  1. Создайте новый файл JS, чтобы поместить в него компоненты.
  2. Экспортируйте компонент из этого файла (используя экспорт по умолчанию или именованный экспорт).
  3. Импортируйте его в файл, в котором вы будете использовать компонент (используя соответствующую технику для импорта по умолчанию или именованного экспорта).
Здесь и Profile, и Gallery были перемещены из App.js в новый файл с именем Gallery.js. Теперь вы можете изменить App.js, чтобы импортировать галерею из Gallery.js:
// App.js

import Gallery from './Gallery.js';

export default function App() {
  return <Gallery />;
}
// Gallery.js

function Profile() {
  return <img src="https://example.com/userpic.jpg" alt="User Name" />;
}

export default function Gallery() {
  return (
    <section>
      <h1>Amazing scientists</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}
Обратите внимание, как этот пример теперь разбит на два файла компонентов:
  1. Gallery.js:
    • Определяет компонент профиля, который используется только в том же файле и не экспортируется.
    • Экспортирует компонент Gallery в качестве экспорта по умолчанию.
  2. App.js:
    • Импортирует Gallery по умолчанию из Gallery.js.
    • Экспортирует корневой компонент App в качестве экспорта по умолчанию.
Вы можете столкнуться с файлами, в которых отсутствует расширение .js, например:
import Gallery from './Gallery';
И './Gallery.js', и './Gallery' будут работать с React, хотя первое ближе к тому, как работают нативные модули ES.

Экспорт по умолчанию и именованный экспорт

Существует два основных способа экспорта значений с помощью JavaScript: экспорт по умолчанию и именованный экспорт. До сих пор в наших примерах использовался только экспорт по умолчанию. Но вы можете использовать один или оба из них в одном файле. Файл может иметь не более одного экспорта по умолчанию, но он может иметь сколько угодно именованных экспортов. То, как вы экспортируете свой компонент, определяет, как вы должны его импортировать. Вы получите сообщение об ошибке, если попытаетесь импортировать экспорт по умолчанию так же, как и именованный экспорт. Эта таблица поможет вам:
СинтаксисЭэкспортированиеИмпортрирование
по умолчаниюexport default function Button() {}import Button from './button.js';
именованныйexport function Button() {}import { Button } from './button.js';
Когда вы пишете импорт по умолчанию, вы можете указать любое имя после импорта. Например, вместо этого вы можете написать import Banana from './button.js', и он все равно предоставит вам тот же экспорт по умолчанию. Напротив, при именованном импорте имя должно совпадать с обеих сторон. Вот почему они называются именованным импортом. Люди часто используют экспорт по умолчанию, если файл экспортирует только один компонент, и используют именованный экспорт, если он экспортирует несколько компонентов и значений. Независимо от того, какой стиль вы предпочитаете, всегда давайте осмысленные имена функциям компонентов и файлам, которые их содержат. Компоненты без имен, такие как export default() => {}, не рекомендуются, поскольку они усложняют отладку.

Экспорт и импорт нескольких компонентов из одного файла

Что, если вы хотите показать только один Profile вместо галереи? Вы также можете экспортировать компонент Profile. Но в Gallery.js уже есть экспорт по умолчанию, и у вас не может быть двух экспортов по умолчанию. Вы можете создать новый файл с экспортом по умолчанию или добавить именованный экспорт для Profile. Файл может иметь только один экспорт по умолчанию, но он может иметь множество именованных экспортов. Чтобы уменьшить возможную путаницу между экспортом по умолчанию и именованным экспортом, некоторые команды предпочитают придерживаться только одного стиля (по умолчанию или именованного) или избегать их смешивания в одном файле. Это вопрос предпочтений. Делайте то, что лучше всего работает для вас. Сначала экспортируйте Profile из Gallery.js, используя именованный экспорт (без ключевого слова default ):
export function Profile() {
  // ...
}
Затем импортируйте Profile из Gallery.js в App.js, используя именованный импорт (с фигурными скобками):
import { Profile } from './Gallery.js';
Наконец, отобразите <Profile /> из компонента App:
export default function App() {
  return <Profile />;
}
Теперь Gallery.js содержит два экспорта: экспорт Gallery по умолчанию и именованный экспорт Profile. App.js импортирует их оба.
// App.js

import Gallery from './Gallery.js';
import { Profile } from './Gallery.js';

export default function App() {
  return <Profile />;
}
// Gallery.js

export function Profile() {
  return <img src="https://example.com/userpic.jpg" alt="User Name" />;
}

export default function Gallery() {
  return (
    <section>
      <h1>Amazing scientists</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}
Теперь вы используете сочетание экспорта по умолчанию и именованного экспорта:
  • Gallery.js:
    • Экспортирует компонент Profile как именованный экспорт с именем Profile.
    • Экспортирует компонент Gallery в качестве экспорта по умолчанию.
  • App.js:
    • Импортирует Profile как именованный импорт с именем Profile из Gallery.js.
    • Импортирует Gallery как импорт по умолчанию из Gallery.js.
    • Экспортирует корневой компонент App в качестве экспорта по умолчанию.

Резюме

На этой странице вы узнали:
  • Что такое файл корневого компонента
  • Как импортировать и экспортировать компонент
  • Когда и как использовать именованный импорт и экспорт; и импорт и экспорт по умолчанию
  • Как экспортировать несколько компонентов из одного файла