Выбор структуры папок в React: семь вариантов организации и точки их неэффективности
Сравните плоскую, типовую, основанную на функциях, Atomic Design, DDD, Feature-Sliced Design и монорепозиторийные структуры для React, а также узнайте, на каком уровне масштабирования каждая из них перестаёт быть эффективной.
Источники: React Folder Structure: 7 Ways to Organize a React App (And Exactly When Each One Breaks)
Любая развивающаяся база кода на React рано или поздно сталкивается с одним и тем же вопросом: куда поместить этот файл? Официального ответа нет, однако команды тратят целые спринты на переход между различными структурами, а собеседники по-прежнему задают вопросы кандидатам на эту тему. В этом руководстве сравниваются семь распространённых способов организации React-приложения — от простой папки src/ до монорепозитория с несколькими приложениями — при этом используется одно ориентировочное приложение на протяжении всего текста. Для каждой структуры вы узнаете, что это такое, кто действительно её использует, в чём её сильные стороны, где возникают проблемы, и в какой момент она перестаёт работать, чтобы вы могли выбрать наименее сложную структуру, подходящую именно вашему приложению.
Никто официальный не выберет её за вас
В документации React версии Legacy когда-то существовала страница, посвящённая структуре файлов. В ней говорилось, что React не имеет мнения относительно того, как располагать файлы в папках, и рекомендовалось тем, кто начинает проект, не тратить более пяти минут на выбор структуры. Эта страница была удалина, когда документация переехала на react.dev, и её ничем не заменили. Сегодня React практически ничего не говорит о папках. Старые рекомендации сохранились на архивной странице FAQ.
Фреймворки и инструменты, связанные с React, также не высказывают чётких рекомендаций. Next.js утверждает, что не имеет мнения относительно того, как организовывать и располагать файлы проекта. Шаблон React от Vite генерирует лишь несколько исходных файлов без каких-либо папок. Ни один из игроков в этой экосистеме не указывает, что следует делать.
Это отсутствие многого объясняет. Такие структуры, как Bulletproof React, набрали десятки тысяч звезд на GitHub в основном за предложение определенной структуры папок, и разработчики постоянно спорят на эту тему. Честное определение всего, что будет сказано далее, заключается в том, что эти семь структур являются конвенциями, а не стандартами. Ни одна из них не поддерживается самим React. Важно понимать преимущества и недостатки каждой из них, а также моменты, в которых они теряют эффективность, потому что именно это становится решающим фактором, когда приходит время создавать реальное приложение.
Два принципа, лежащих в основе любой структуры
Их создатели более спокойны, чем их сторонники
Люди, которые создали эти методологии, обычно относятся к ним гораздо свободнее, чем те, кто их продвигает. Брэд Фрост, создатель Atomic Design, опубликовал книгу «Extending Atomic Design» в 2019 году. В ней он рассказывает о людях, обращавшихся к нему с просьбой «исправить» его терминологию, настаивает на том, что атомный дизайн — это не жесткое догма, и говорит, что командам, желающим использовать девять этапов, названных в честь участников группы Chicago, следует это делать. Её можно прочитать по ссылке Extending Atomic Design. Создатель методологии говорит, что названия не имеют значения, в то время как где-то процесс ревью кода застопорился из-за вопроса о том, считается ли тот или иной компонент молекулой.
В старой документации React также упоминался Atomic Design. В ней отмечалось, что некоторые разработчики делят компоненты на папки в зависимости от их роли в приложении, приводился Atomic Design в качестве примера такого подхода, и рекомендовалось рассматривать подобные методологии как полезные примеры, а не строгие правила. Поскольку это исходит из собственной документации React, это лучшая имеющаяся критика строгого применения Atomic Design в React.
Screaming Architecture
В этой дискуссии на самом деле существует лишь один весомый аргумент, и он появился ещё до React. В сентябре 2011 года Роберт К. Мартин опубликовал статью Screaming Architecture. Его вопрос заключался в следующем: когда кто-то смотрит на верхние уровни директорий вашего приложения, сразу ли становится ясно, что речь идет о системе здравоохранения, бухгалтерской системе или системе управления запасами? Его вывод заключался в том, что архитектура должна рассказывать читателю о самой системе, а не о фреймворках, которые она случайно использует.
Эта идея лежит в основе любой структуры, основанной на компонентах. Папка src/, содержащая папки components/, hooks/, utils/ и services/, указывает на то, что речь идет о приложении на React. Папка src/, содержащая папки checkout/, billing/ и inventory/, показывает, чем занимается бизнес. Каждая структура, описанная в этом руководстве, является либо вариацией этой идеи, либо ее сознательным отклонением.
Колокация
Второй принцип имеет наибольшее значение в повседневной работе. Кент К. Доддс называет его колокацией: храните код как можно ближе к месту, где он используется. Он приписывает Дэну Абрамову более точное формулирование — элементы, которые меняются вместе, должны находиться как можно ближе друг к другу. Полное обоснование приведено в статье Colocation.
С этой точки зрения каждая структура в данном руководстве отвечает на один вопрос: какие части вашего кода развиваются как единое целое? Если вы ответите на него правильно, папки практически сами собой сформируются. Если же ответ будет неверным, никакая методология вас не спасет.
Примечание к популярной цитате: совет «перемещать файлы до тех пор, пока всё не будет казаться правильным» часто приписывают Дэну Абрамову и иногда цитируют как официальную документацию React. На самом деле это не является частью документации; в документации приводится совет, занимающий пять минут на рассмотрение. Цитата Абрамова восходит к твиту, который, по-видимому, был удален. Сама идея разумна; рассматривайте эту цитату как народную примету.
Одно приложение-образец, семь вариантов структуры
Сравнение макетов целесообразно лишь в том случае, если приложение остается неизменным; в противном случае вы сравниваете не структуры, а примеры реализации. Поэтому все приведенные ниже макеты используют одно и то же небольшое электронное магазинное приложение с одинаковыми файлами:
- Функции: аутентификация (вход и регистрация), каталог (список товаров и их описание), корзина, процесс оплаты и заказы.
- Примитивы интерфейса:
Button,Input,Modal,BadgeиSpinner. - Хуки:
useAuth,useCart,useProducts,useCheckoutиuseDebounce. - Модули API:
authApi,productsApi,checkoutApi,ordersApiи общий модульapiClient. - Прочее: помощники
cartStoreиformatPrice.
Четыре из этих файла созданы намеренно сложными, поскольку показывают, в каких моментах каждая схема становится проблематичной:
Modal.jsxполучает метку «атом», «молекула» или «организм» в зависимости от того, кто его классифицирует, что подчеркивает таксономию Atomic Design.useDebounce.jsявляется совершенно универсальным, но им пользуется только поиск в каталоге. Является ли он глобальным хуком или хуком каталога?CartTotal.jsxотображается как в выдвижном меню корзины, так и на экране оформления заказа, поэтому неясно, кто является его владельцем.formatPrice.jsкажется простым утилитным файлом, если только деньги не являются реальным концептом в данной области, в котором случае он должен быть объектом типаMoney, и размещение его в папке с утилитами — ошибка.
Помните об этих четырёх принципах. Каждая структура решает их по-разному, и именно эти различия отличают одну схему организации от другой.
Плоская структура
Что это такое
Все файлы находятся непосредственно в папке src/, без или почти без дополнительных папок, и расположены в алфавитном порядке, поскольку ничто другое не задаёт определённого порядка.
Никто не представлял это в виде формальной методологии. Это просто место, откуда начинается каждый проект, и в котором остаётся немало проектов. Даже документация React рекомендовала её в качестве отправной точки: если вы чувствуете, что зашли в тупик, храните все файлы в одной папке, а когда она станет слишком большой и потребуется разделение, вы поймёте, какие файлы обычно редактируете вместе. Это действительно хороший совет. Структура, сформированная на основе наблюдаемых закономерностей изменений, лучше той, которая навязывается с самого начала.
Кто её использует
Прототипы, экспериментальные решения, демо-версии в среде тестирования, проекты хакатонов, домашние задания и внутренние инструменты с одним администратором. Кроме того, в нем описано множество приложений, используемых в производстве, которые так и не были переписаны заново.
Официальный шаблон Vite React по сути является простым. Выполнение команды npm create vite@latest -- --template react-ts генерирует файлы main.tsx, App.tsx, App.css, index.css и папку assets/; папок components/ или features/ нет. Поскольку этот шаблон не выражает никаких предпочтений, дискуссии по этому вопросу переходят к каждой команде, которая им пользуется.
Преимущества
- Навигация не требует дополнительных затрат. Существует всего одна папка, а инструмент выбора файлов находит любой файл по имени. Поскольку не требуется принимать решения относительно расположения элементов, споров по этому поводу тоже нет.
Недостатки
- Масштабируемость резко снижается примерно на уровне 30 файлов. Проблема возникает не постепенно: однажды список файлов больше не помещается в боковую панель, и форма приложения исчезает.
- Алфавитный порядок не имеет смысла.
authApi.jsнаходится рядом сApp.jsxисключительно потому, что оба начинаются на букву A, в то время какuseAuth.jsиLoginForm.jsx», которые вы всегда редактируете вместе, оказываются далеко друг от друга. Порядок сортировки противоречит логике расположения элементов рядом. - Не существует правил, которые можно было бы применить. Любой файл может импортировать любой другой, циклические зависимости возникают незаметно, и к тому моменту, когда вы хотите установить ограничения, каждое нарушение уже имеет серьезные последствия.
- Удаление какой-либо функции превращается в настоящее расследование. Чтобы удалить процесс оплаты, необходимо прочитать все файлы, чтобы понять, какие из них к нему относятся, поскольку для удаления нет отдельной папки.
Где возникают проблемы
Примерно при 30 файлах или с момента присоединения второго разработчика — при этом второй порог становится более значимым. Плоская структура работает, потому что один человек помнит всю структуру в своей голове. У двух человек появляются две версии структуры, которые сразу же начинают отличаться.
Структура на основе типов
Что это такое
Файлы группируются по их назначению: компоненты вместе, хуки вместе, вызовы API вместе, сторы вместе. В документации React предыдущих версий это описывалось как группировка по типу файла, и её представляли как один из двух распространенных подходов без выдвижения преимуществ того или иного. Для приложения-образца это означает, что Button, Modal, CartItem, CheckoutForm и все остальные компоненты находятся в папке components/, все хуки — в hooks/, все модули API — в api/, а cartStore — в store/.
Поскольку большинство учебников обучают именно этому подходу, он также чаще всего встречается в реальных проектах.
Кто им пользуется
Почти все учебники по React, большинство проектов из буткэмпов и значительная часть продакшн-приложений развивались за счёт отказа от примитивной структуры без чёткого плана. Это самый простой шаг: как только папка src/ заполняется лишними файлами, первой естественной реакцией становится создание папки components/.
Преимущества
- В этом не нуждается никакое объяснение. Никому никогда не приходилось объяснять, что должно находиться в папке
hooks/, поэтому новые разработчики могут начать работу с первого дня, используя уже знакомые им термины. - Классификация происходит легко: компоненты помещаются в
components/, хуки — вhooks/, а сам файл указывает на его тип. Посмотрите в сравнение с Atomic Design, где даже разумные люди могут часами спорить о структуре одного компонента. - Эта модель вполне подходит для небольших проектов. При количестве файлов менее 50 она работает отлично, а переход на другую структуру принесёт больше ущерба, чем пользы.
Недостатки
- Он объявляет о фреймворке, а не о продукте, что именно противоречит целям подхода Screaming Architecture. Каталоги верхнего уровня с именами
components/,hooks/,api/иutils/говорят новичку лишь о том, что используется React, но ничего не рассказывают о функциях приложения. Интернет-магазин и система планирования в больнице с точки зрения структуры каталогов выглядят одинаково.
CartItem.jsx, находящегося в папке components/, файла useCart.js в папке hooks/, файла cartApi.js в папке api/ и файла cartStore.js в папке store/. Это четыре директории и четыре позиции прокрутки для одного логического изменения. Принцип соседства файлов нарушается: файлы, которые с наибольшей вероятностью изменятся в рамках одного коммита, размещаются как можно дальше друг от друга.components/ превращается в новую плоскую структуру. В эталонном приложении в ней оказывается около 18 файлов без внутренней группировки. Проблема плоской структуры решается для хуков и модулей API, но затем снова возникает внутри папки, содержащей основную часть кода.ProductCard импортировать useCheckout; эти папки носят исключительно декоративный характер.Где возникают проблемы
Примерно при 100 файлах, или когда в папке components/ появляется около 20 элементов, начинаются проблемы. Сигналом опасности является изменение поведения: разработчики начинают добавлять префиксы к именам файлов, таких как CartItem, CartDrawer и CartTotal, чтобы имитировать группировку, которую не обеспечивают папки. Эти префиксы — своего рода запрос кодовой базы на создание папок для отдельных функций.
Структура, основанная на функциях
Что это такое
Файлы группируются по их функциям. Каждая функциональность содержит свои компоненты, хуки, вызовы API и состояние в одном месте; общие примитивы находятся в специальной зоне, а функциональности объединяются на уровне приложения без взаимных импортов.
В старых документациях React такая группировка называлась по функциональностям или маршрутам, и предлагался практический способ определения границ. Поскольку значение термина «функциональность» не является универсальным, вы сами выбираете степень детализации; если у вас не получается составить список верхнего уровня папок, ваши пользователи могут описать основные части продукта и использовать свою собственную модель в качестве основы. Пожалуй, это самый полезный практический совет по всей этой теме: ваши пользователи уже знают, где проходят границы функциональностей.
По сути, это Screaming Architecture, переведённое на React, и сегодня оно является основным выбором. В примерном приложении папка features/cart/ содержит компоненты корзины, функцию useCart и API корзины, а также объект cartStore; папка features/checkout/ содержит компонент CheckoutForm, функцию useCheckout и API оплаты, и так далее; компоненты Button, Modal и другие базовые элементы находятся в общей папке components/.
Кто им пользуется
Самая известная реализация в качестве ориентира — это Bulletproof React от Алана Аликовича, которая имела около 35,5 тыс. звезд на GitHub и на момент написания статьи по-прежнему активно обслуживалась. Это самая распространённая структура проектов на React, и документ о структуре проекта Bulletproof React стоит прочитать напрямую, вместо того чтобы полагаться на косвенные краткое изложения.
Рекомендуемая структура папок src/ включает ровно одиннадцать верхних уровней папок, перечисленных ниже с указанием их назначения. Обратите внимание, что только одна из них, features, организована по тематическим категориям; остальные представляют собой общие слои, которые используются функциями проекта, плюс слой app, который объединяет всё воедино.
src
+-- app # application layer (routes, app.tsx, provider.tsx, router.tsx)
+-- assets # static files such as images, fonts, etc.
+-- components # shared components used across the entire application
+-- config # global configurations, exported env variables etc.
+-- features # feature based modules
+-- hooks # shared hooks used across the entire application
+-- lib # reusable libraries preconfigured for the application
+-- stores # global state stores
+-- testing # test utilities and mocks
+-- types # shared types used across the application
+-- utils # shared utility functions
Внутри каждой функции может быть до семи сегментов: api, assets, components, hooks, stores, types и utils. В документации четко указано, что функции должны содержать только те сегменты, которые они действительно используют.
Два правила выполняют основную работу:
- Отсутствие импортов между функциями. Рекомендации проекта Bulletproof React запрещают импорт функций одной из другой и советуют создавать композиции функций на уровне приложения, чтобы каждая из них оставалась независимой и код становился более понятным.
- Однонаправленный поток. Код должен двигаться в одном направлении — от общего кода к функциям, а затем к приложению (
shared -> features -> app).
Оба правила можно применять с помощью правила ESLint import/no-restricted-paths. Именно этот шаг применения часто игнорируют многие команды, и обычно именно поэтому со временем их границы становятся менее четкими.
Изменения в файлах типа barrel: ранее библиотека Bulletproof React рекомендовала использовать файлы barrel —
index.js, который повторно экспортирует публичные элементы функционала. Теперь она рекомендует импортировать файлы напрямую, поскольку файлы barrel могут мешать процессу оптимизации в Vite и вызывать проблемы с производительностью. Если вы изучали этот подход по более старым руководствам, советы теперь изменились. Поскольку файлы barrel ранее использовались большинством команд для представления публичного API функционала, отказ от них означает, что границы теперь полностью определяются конфигурацией lint-инструмента.
Преимущества
- Колокация файлов осуществляется автоматически. Одно изменение в корзине затрагивает только одну папку, поскольку файлы, которые изменяются вместе, находятся рядом друг с другом.
rm -rf. Удалите папку features/checkout/, устраните проблемы с импортами, на которые жалуется компилятор, и всё готово. Это важнее, чем кажется: код, который трудно удалить, обычно никогда не удаляется.auth/, catalog/, cart/, checkout/ и orders/, понимает суть продукта ещё до того, как откроет хотя бы один файл.Недостатки
- У понятия «фича» нет четкого определения, поэтому будут споры по этому поводу. Файл
CartTotal.jsxиспользуется как в корзине, так и при оформлении заказа. Ни один из вариантов не является правильным; просто нужно принять решение, которое, скорее всего, придется пересмотреть позже. - Для общего кода каждый раз требуется собственное решение. В настоящее время функция
useDebounceприменяется только при поиске в каталоге. Если сделать её глобальной, папка с общими хуками заполнится элементами, используемыми лишь один раз. Лучше оставить её в папкеfeatures/catalog/hooks/, а когда потребуется в разделе оплаты, её придется скопировать, импортировать в другие разделы (что запрещено) или повысить её уровень. - Чрезмерное разделение на фичи раньше времени наносит ещё больший ущерб, чем чрезмерная абстракция. Размещение приложения из 40 файлов в пяти папках фич создает лишь формальности, а не структуру.
Где возникают проблемы
В большинстве случаев проблем нет, именно поэтому этот подход является стандартным. Однако он становится проблематичным в двух ситуациях. Во-первых, когда одна функция становится очень большой: папка checkout/ с 80 файлами представляет собой лишь структуру, основанную на типах, на один уровень ниже. Во-вторых, когда появляется реальная модель домена, которая не совпадает с функциями интерфейса. Эти два фактора приводят к появлению следующих трех структур.
Atomic Design
Что это такое
Atomic Design — это методология Брэда Фроста, представленная в блог-посте в июне 2013 года и дополненная книгой в 2016 году. Она определяет пять уровней. Перефразируя его определения:
- Атомы — это основные строительные блоки, из которых состоит любой интерфейс.
- Молекулы — это довольно простые группы элементов интерфейса, которые работают вместе как единое целое.
- Организмы — это более сложные компоненты, созданные из молекул, атомов и даже других организмов.
- Шаблоны — это структуры на уровне страниц, которые располагают компоненты в определенном макете и описывают структуру содержимого.
- Страницы — это конкретные примеры шаблонов, заполненные реальным, типичным содержимым.
Деталь, которую почти все игнорируют, — это собственное описание цели методологии Frost. Он представляет Atomic Design как ментальную модель, а не линейный процесс: способ видеть интерфейс одновременно как целостное единство и совокупность отдельных частей. Это никогда не было структурой папок; сообщество добавило ее позже.
Кто им пользуется
Здесь реальность отличается от сложившегося мнения. Существует мало проверяемых доказательств того, что крупные системы дизайна на React используют строгую таксономию папок atoms/, molecules/, organisms/. Например, Shopify Polaris и GitHub Primer не применяют такое название папок, вопреки результатам поиска, указывающим на обратное. Явным примером может служить Pattern Lab — собственный инструмент Frost, созданный для демонстрации этого метода.
В качестве термина среди команд, работающих над системами дизайна, Atomic Design обладает огромным влиянием. Однако когда речь идет о фактической структуре папок в производственных приложениях на React, доказательства его использования гораздо слабее, чем позволяет предположить его известность. Если кто-то утверждает, что крупная компания структурирует свои папки в React именно так, попросите показать репозиторий.
Преимущества
- Это предоставляет дизайнерам и инженерам общий язык. В этом и заключается настоящая польза, причем папки здесь не играют никакой роли. Если дизайнер упоминает тот или иной элемент, инженер сразу понимает его примерные границы, что очень ценно в командах по разработке дизайн-систем.
- Это способствует композиции. Иерархия побуждает создавать небольшие компоненты и объединять их, что является хорошей практикой в React независимо от того, где находятся файлы.
- Это позволяет увидеть весь набор примитивов. Папка
atoms/ясно показывает, когда у вас есть три варианта кнопок, которые на самом деле должны быть одним.
Недостатки
- Классификация действительно сложна, и её создатель это признаёт.
Modal.jsxобъединяет фон, контейнер, заголовок и элемент для закрытия. Является ли это группой атомов, то есть молекулой, или сложным самостоятельным элементом, то есть организмом? В конце концов Фрост опубликовал правило: если разбор компонента приводит к базовым элементам, таким как изображения, заголовки, абзацы и кнопки, это молекула; если же получаются более мелкие компоненты, такие как карточки, списки для обмена или медиаобъекты, это организм. Подобные эвристики существуют лишь потому, что границы неоднозначны, и каждая неоднозначная граница становится предметом постоянных дискуссий, не приносящих никакой пользы пользователю.
Button и CheckoutForm, но совершенно не предусматривает концепции useCheckout, checkoutApi, cartStore или formatPrice. Половина файлов не входит в эту таксономию, поэтому командам приходится использовать элементы типа «атом», «молекула» и «организм», а также типобазированную структуру для всего остального, что приводит к наследованию обоих наборов проблем.organisms/, templates/, pages/, hooks/ и api/.Где возникают проблемы
Как только это подход начинает применяться вне системы дизайна. Располагать примитивы по степени сложности внутри папок packages/ui/ или shared/ui/ — разумно. Однако при применении ко всему приложению такой подход сразу же теряет эффективность, поскольку основная часть любого приложения состоит не из UI-компонентов.
Собственная позиция Фроста подтверждает такое толкование: он называет это ментальной моделью, а не догмой, и предлагает командам переименовать этапы. Команды, которые строго соблюдают принципы использования атомов и молекул в React-приложениях, проявляют большую жесткость, чем сам создатель этого метода.
Структура, основанная на домене
Что это такое
Подход Domain-Driven Design впервые был описан Эриком Эвансом в его книге 2003 года, написанной более чем за десять лет до появления React, и представляет собой способ моделирования сложных бизнес-доменов с помощью кода. Два из его принципов находят применение и в фронтенде. Ограниченный контекст — это область программного обеспечения, в которой определенные термины, определения и правила применяются единообразно. Принцип универсального языка гласит, что код должен использовать тот же словарный запас, что и бизнес-процессы.
При применении к React-приложению каждый домен становится верхним уровнем папки, внутри которой делится на слои:
domain/содержит сущности и объекты значений: чистые бизнес-правила.application/содержит случаи использования.infrastructure/содержит клиенты API и механизмы хранения.presentation/содержит компоненты React.
Зависимости указывают только внутрь, поэтому сфера домена ничего не знает о React или HTTP.
Посмотрите, что происходит с неудобными файлами. formatPrice больше не является вспомогательным функцией; он превращается в объект значение Money, находящийся в папке catalog/domain/. useAuth сокращается до простого обертки на уровне представления над случаем использования loginUser. React намеренно оставляется на периферии.
Кто им пользуется
Самым надежным практическим руководством является книга Алекса Беспоясова Clean Architecture on Frontend, в которой приводится полная реализация на React и TypeScript в репозитории bespoyasov/frontend-clean-architecture. По его объяснению, слой доменной логики остается неизменным независимо от того, переходите ли вы с React на Angular или меняетесь конкретный случай использования.
Также стоит знать, что Манфред Штайер, возможно, самый известный сторонник применения принципов DDD на фронтенде, в основном работает с Angular. Модель Angular, основанная на классах и ориентированная на сервисы с активным использованием внедрения зависимостей, лучше всего подходит для реализации принципов DDD по сравнению с функциями и хуками в React. Прежде чем полностью принимать какое-либо подробное руководство по применению DDD на фронтенде, убедитесь, для какой именно фреймворк-среды оно написано.
Такой стиль используется в сферах финтеха, страхования, здравоохранения, логистики и корпоративного программного обеспечения B2B, где бизнес-правила являются сложными и существовали задолго до появления интерфейса.
Преимущества
- Бизнес-логика остается актуальной даже после изменения фреймворка. Энтитет
Cartс правиламиaddItemпредставляет собой обычный TypeScript; замена React не повлияет на слой бизнес-логики. Для продукта, предполагаемого к использованию десять лет, это является значительным преимуществом. - Правила можно тестировать без React. Нет ничего, что нужно было бы отрисовывать, нет хуков и библиотек тестирования — только функции и утверждения, выполняющиеся за доли секунды. При наличии сложных правил это само по себе может стать основанием для такой структуры.
- Код отражает бизнес-процессы. Когда эксперт в области бизнеса говорит, что заказ не может быть отправлен до проверки адреса, это правило находится исключительно в файле с названием
Order.ts.
Недостатки
- Объем шаблонного кода велик, и это является основной стоимостью. Каждая сущность обычно включает модель домена, DTO и маппер между ними: три файла и слой преобразования для того, чтобы функционал приложения представлялся в виде единого типизированного ответа API. Это количество умножается при наличии двадцати сущностей.
- У большинства приложений на React отсутствует реальный домен. Это скорее суждение, чем подтверждённый факт, но именно в этом суть данного раздела. Типичное приложение на React отображает данные из API, принадлежащего другой команде или сервису, и его настоящая сложность заключается в состоянии сервера, кэшировании, оптимистичных обновлениях и ситуациях конкуренции. Подход DDD не решает ни одной из этих проблем; библиотека для управления состоянием сервера, такая как React Query, решает фактическую проблему лучше, чем слой домена. Если ваши сущности представляют собой типизированные ответы API без инвариантов, такая архитектура стоит денег, но не обеспечивает никакой защиты.
- Фронтенд на самом деле не может полностью избавиться от своей фреймворковой зависимости. Обещается независимость от фреймворка, но фронтенд всегда связан с моделью отображения, механизмами навигации, паттернами управления состоянием, API браузера и инструментами сборки. Чистая архитектура лишь перемещает эти ограничения на периферию, а не устраняет их. Это действительно преимущество, хотя и меньшее, чем обещается.
- Контексты пользовательского интерфейса нечетко определены. На бэкенде четко прослеживается владение данными. Фронтенд отображает только то, что необходимо на экране, причем одна страница часто охватывает три контекста, поэтому границы становятся субъективными и постоянно меняются.
- Интеграция новых сотрудников занимает время. Разработчику на React, который никогда раньше не сталкивался с DDD, может потребоваться несколько недель, чтобы стать продуктивным, и кто-то всегда будет спрашивать, зачем вообще нужен маппер.
Где возникают проблемы
Когда домен существует лишь на бумаге. DDD предназначена для решения проблем, связанных с реальной сложностью бизнес-процессов. Если ваши неизменяемые элементы реальны — такие как деньги, контракты, регулируемые рабочие процессы или многоэтапные автоматы состояний, управляемые правилами, — инвестиции окупаются. Когда задача заключается в отображении списка продуктов с определенного конца, вы покупаете множество излишних формальностей для обертки функции fetch.
Полезный тест: можете ли вы перечислить три бизнес-правила, более интересных, чем проверка обязательных полей? Если нет, то у вас есть лишь интерфейс, а не домен.
Дизайн с фрагментацией по функциям
Что это такое
Дизайн с фрагментацией по функциям (FSD) — единственная структура здесь, которая представляет собой формальное описание, включающее версионированные выпуски, документацию и официальный инструмент проверки архитектуры под названием steiger. Остальные являются конвенциями; FSD записана письменно.
В нём существует три уровня иерархии:
- Слои — это фиксированные верхние папки:
app,pages,widgets,features,entitiesиshared. - Ломтики — это группы внутри слоя, разделённые по бизнес-значению, то есть в зависимости от того, что код означает для продукта или приложения.
- Сегменты — это группы внутри ломтика, разделённые по технической цели:
ui,api,model,libиconfig.
Правило, которое объединяет всё это, заключается в том, что модуль в ломтике может импортировать другие ломтики только если они находятся строго ниже его собственного слоя. Импорты всегда направлены вниз, и два ломтика, находящихся на одном слое, никогда не могут импортировать друг у друга код. Между сиблингами отсутствует любая связь, что обеспечивается инструментом проверки кода, а не добрыми намерениями.
Стоит знать два уточнения к утверждениям, распространяемым в других источниках:
- Слой
processesсчитается устаревшим. В документации указано, что текущие спецификации рекомендуют избегать его использования и переносить его содержимое в слоиfeaturesиapp. Руководства, в которых по-прежнему перечисляются семь слоев, включаяprocesses, описывают более старую версию. - Слой
pagesне был устаревшим; его статус был повышен. Версия 2.1, выпущенная в ноябре 2024 года, сопровождается заголовком «Pages come first!». Теперь рекомендуется хранить больше кода внутри страниц: крупные блоки интерфейса, формы и логика обработки данных, которые не используются другими страницами, должны оставаться внутри соответствующей страницы.
В реализации FSD для этого оригинального приложения файл CheckoutForm.tsx находится в папке pages/checkout/ui/, а не в отдельном модуле функционала. Подход версии v2.0 предусматривал разделение кода на сущности и функции по принципу, тогда как версия v2.1 сохраняет его внутри страницы до тех пор, пока другая страница фактически не потребует его части.
Мотивация такого изменения — самая интересная часть. Разделение кода по сущностям в версии v2.0 приводило к слабой кохезии: для отслеживания одного пользовательского потока приходилось просматривать множество папок, а ненужный код было трудно обнаружить. Разработчики фактически признали, что в более ранней версии требовалось слишком много архитектурных решений ещё до написания кода. Самая серьезная критика FSD была высказана и рассмотрена его собственными авторами. Следует отдать должное за внедрение исправления, но команды, которые использовали v2.0, заплатили за этот урок.
Кто им пользуется
Ситуация с внедрением этой технологии требует более честного анализа. На момент написания статьи на главной странице FSD есть раздел для компаний, использующих FSD, в котором не указаны никакие конкретные компании; вместо этого посетителей просят сообщить о своей компании.
На официальной странице с примерами перечислено около 21 проекта, в основном демо-версии и приложения для хобби, такие как клон RealWorld, игра в Судоку, фиктивный интернет-магазин и небольшая игра в браузере. Проекты, которые выглядят как готовое программное обеспечение, также являются небольшими: Roke.to, Moke Smoke, Moonlogs и расширение для Chrome.
По состоянию на момент написания показатели внедрения остаются скромными, но реальными: около 297 хранилищ на GitHub используют тег feature-sliced-design, хранилище документации имеет примерно 2,3 тысячи «звезд», а инструмент проверки кода — около 388. Сообщество сосредоточено в русскоязычном сообществе разработчиков, из-за чего масштабы использования этой технологии на английском языке кажутся меньшими, чем они есть на самом деле.
Ни одна крупная, известная западная компания не была подтверждена в качестве пользователя FSD на основе первоисточников, и ни в одной официальной документации не указан создатель этой методологии. Для столь формализованного подхода это необычно и заслуживает учета при принятии решения о его использовании.
Преимущества
- Он прекращает споры о местоположении элементов, поскольку спецификация уже определяет это заранее. Куда поместить этот файл? Спецификация отвечает на этот вопрос. Это экономит реальное время в крупных командах, и именно здесь FSD явно превосходит подход, основанный на функциях, при котором определение функций постоянно пересматривается.
- Границы проверяются машинами.
steigerи настройки ESLint обеспечивают соблюдение правил импорта, и архитектура, находящаяся под контролем, развивается гораздо медленнее, чем архитектура без такого контроля. - Этот подход позволяет работать с крупными командами при минимальной координации. Фиксированные слои и отсутствие взаимных импортов позволяют командам работать параллельно без конфликтов.
product — это то, что существует; add-to-cart — это действие пользователя. Это разные типы кода, но большинство схем смешивают их.Недостатки
- Кривая обучения крутая, и на странице с описанием целей FSD сама признаёт эту проблему, отмечая, что существующие подходы имеют высокий порог вхождения, что затрудняет найм специалистов. FSD не является исключением: перед тем как поместить хотя бы один файл, необходимо изучить три типологии — слои, сегменты и части.
- Переход на версию v2.1 свидетельствует о изменениях. Изменилась основная единица разбиения, и всем, кто работает по спецификации, приходится следовать за её обновлениями.
- Сама документация называет этот подход чрезмерным для небольших проектов, отмечая, что он не нужен для краткосрочных проектов или там, где скорость важнее качества кода.
- Доказательств эффективности применения недостаточно. Это приемлемо для личного проекта, но затрудняет продажу идеи, когда заинтересованные стороны спрашивают, кто ещё использует этот инструмент в реальных проектах.
- Миграция устаревшей базы кода стоит дорого. Доработка существующего кода новыми слоями и компонентами — это масштабный проект с незначительными видимыми результатами.
Где возникают проблемы
В небольших командах и при краткосрочных приложениях, где формальности превалируют над реальной пользой. FSD предназначен для крупных, долговечных продуктов с большим количеством участников, которым действительно необходимы четкие границы. Для более мелких проектов структура, основанная на функциях, обеспечивает большую часть преимуществ при использовании гораздо меньшего количества концепций.
Модульная структура и монорепозитории
Что это такое
В отличие от остальных случаев, здесь речь не идет об упорядочивании папки src/, поэтому этот вопрос рассматривается в последнюю очередь. Речь идет о структуре репозитория: несколько приложений и общих пакетов находятся в одном репозитории, причем для координации их работы используется запускатель задач.
В документации Turborepo описывается стандартная структура: сначала приложения и сервисы размещаются в папке apps/, а все остальное, такое как библиотеки и инструменты, — в папке packages/. Ознакомьтесь с руководством Turborepo по структурированию репозитория.
Каждое приложение внутри папки apps/ по-прежнему требует внутренней структуры, соответствующей одной из шести возможных. Монорепозиторий не связан с способом организации отдельного приложения, и путаница между ними является наиболее распространенным недопониманием в этой теме.
На момент написания этого текста основными инструментами были turbo (версия 2.10.x, примерно 73 миллиона ежемесячных загрузок), nx (версия 23.x, примерно 38 миллионов) и lerna (версия 9.x, примерно 7,5 миллиона). Перед тем как полагаться на эти цифры, проверьте их актуальное значение. Вопреки распространённому мнению, Lerna не считается устаревшим. В 2022 году ответственность за него перешла к Nx, поскольку существовала опасность того, что он перестанет поддерживаться. Lerna продолжает выпускаться, но теперь передаёт задачи по выполнению и кэшированию Nx, сосредотачиваясь при этом на версионировании и публикации.
Кто им пользуется
В обсуждениях монорепозиториев часто упоминают Google, Meta и Microsoft, что вводит в заблуждение. Это монорепозитории всей компании, а не специально для React, и они ничего не говорят о вашем React-приложении.
Цифры по-прежнему интересны. По данным исследования, хранилище Google насчитывало примерно миллиард файлов и 86 ТБ (Potvin и Levenberg, Communications of the ACM, июль 2016 г.). Meta утверждает, что её хранилище, управляемое Sapling, содержит десятки миллионов файлов, коммитов и веток. По словам Брайана Гарри в мае 2017 года, хранилище Windows в Microsoft хранило около 3,5 миллиона файлов общим объёмом 300 ГБ, над которыми работали примерно 4 000 инженеров.
Каждой из этих компаний пришлось разрабатывать собственные инструменты для решения подобных задач: Google создал Piper и Bazel, Meta — Sapling и EdenFS, а Microsoft — GVFS. Считайте это сигналом о затратах, а не одобрением. Если монорепо требует специализированной команды по инфраструктуре для нормального функционирования, это говорит о некоторых особенностях монорепо.
Соответствующие, проверяемые примеры с React отличаются:
- Репозиторий Vercel с
next.jsпредставляет собой настоящий React-монорепозиторий, включающий папкиapps/иpackages/на верхнем уровне, рабочие пространства pnpm и 19 пакетов, среди которыхnext,create-next-appиeslint-config-next. - Shopify хранит свои приложения на React Native в одном монорепозитории, охватывающем сервисы Shop, Inbox, Point of Sale, Shopify Mobile и Local Delivery. Их инженерная команда объяснила в сентябре 2021 года, что использование единого монорепозитория снижает затраты на техническое обслуживание, поскольку необходимо улучшать и оптимизировать только один процесс, а не поддерживать множество отдельных пакетов и репозиториев.
Вот что на самом деле означает использование монорепозитория, причем речь идет о процессе технического обслуживания, а не об организации кода.
Преимущества
- Изменения между пакетами являются атомарными. Изменение
packages/ui/Buttonи всех трех приложений, которые им пользуются, происходит в рамках одного коммита, одного pull request, одной проверки и одной сессии CI. При использовании отдельных репозиториев это приводит к повышению версии, публикации и трём обновлениям зависимостей. Именно по этой причине существуют монорепозитории. - Совместное использование имеет чёткие границы.
packages/uiимеет собственный файлpackage.jsonи явно определённый публичный API, причём границы пакетов представляют собой самый надёжный механизм в JavaScript. - Существует только одна версия всего: ни одно приложение не остаётся на React 18, пока другие используют версию 19; есть один файл lockfile и один единственный источник правды.
- Возможны крупные рефакторинги. Вы можете переименовать API во всех приложениях-потребителях в рамках одного коммита, при этом компилятор проверяет результат.
Недостатки
- Инструментарий представляет собой реальный и постоянный «налог»: рабочие пространства, графы задач, удаленное кэширование и матрицы CI. Сама документация Turborepo признает проблему масштабирования, поскольку каждое рабочее пространство влечет за собой собственные задачи на тестирование, проверку кода и сборку, и один монорепозиторий может в итоге содержать тысячи таких задач. Turborepo создан для решения проблем, возникающих из-за использования монорепозиториев.
- Процесс CI становится медленнее и дороже, прежде чем ускориться. Без правильно настроенного обнаружения изменений и кэширования каждый pull request запускает все процедуры.
- Интеграция новых сотрудников затрудняется. Перед первым коммитом им необходимо изучить, как работают рабочие пространства, как функционирует граф задач и как ведет себя кэширование.
packages/ui появляется три потребителя, любые изменения рискуют привести к сбоям в трех приложениях. Команды часто реагируют на это добавлением дополнительных параметров и вариантов, пока общий компонент не превращается в язык конфигурации, которого никто не понимает.Где возникают проблемы
Когда существует только одно приложение. Монорепозиторий, в котором папка apps/ содержит единственное приложение, вынужден платить полную стоимость инструментов без какой-либо отдачи: нет кода для совместного использования, а также невозможно осуществлять атомарные изменения между приложениями.
Что касается пороговых значений, то авторитетных данных не существует. Turborepo и Nx не публикуют информации о размере команды или количестве приложений, при которых внедрение становится оправданным. Точные цифры, распространяемые в Интернете, такие как определенное количество инженеров, которое якобы требует использования Turborepo, имеют своим источником некачественный SEO-контент, а не документацию поставщика или исследования. Любой, кто приводит точный порог, просто его выдумывает.
Разумное правило, представленное скорее как мнение, чем как факт: используйте monorepo, когда у вас есть два реальных приложения, которые действительно делит код, или одно приложение вместе с системой дизайна, которую используют команды извне. В остальных случаях затраты превышают выгоду, и ваша собственная ситуация может изменить этот баланс.
React Server Components не определяют структуру папок
Во многих недавних статьях утверждается, что серверные компоненты React меняют способ структурирования папок, обычно рекомендуя использовать отдельные каталоги components/server/ и components/client/. Официальная документация не подтверждает это.
Директива use client обозначает границу в графе модулей, а не в дереве каталогов. Соответствующий руководство по отрисовке в Next.js описывает use client как инструмент для указания границы между графами модулей сервера и клиента, поясняя, что как только файл содержит эту директиву, все его импорты и компоненты, которые он отображает напрямую, попадают в клиентский бандл.
Второй момент является ключевым. Граница распространяется через импорты, а не через папки. Файл внутри каталога client/ не считается компонентом клиента из-за своего местоположения, а файл внутри server/ не получает никакой защиты благодаря этому названию. Решающим фактором являются директивы вместе с цепочкой импортов.
Если вы хотите обеспечить соблюдение правил, для этого используются npm-пакеты server-only и client-only, которые приводят к сбою сборки, когда код пересекает неправильную границу. Папки не обеспечивают такой защиты.
Официальные рекомендации на самом деле указывают в противоположном направлении: необходимо перемещать директиву 'use client' вниз по иерархии, к листовым компонентам, чтобы сделать пакет клиента меньше по размеру. Разделение на верхнем уровне на папки сервера и клиента поощряет противоположный подход.
Несколько конвенций App Router действительно влияют на организацию кода, и они реальны:
- Группы маршрутов, обозначаемые как
(папка), организуют маршруты без изменения URL. - Частные папки, обозначаемые как
_папка, полностью исключают возможность маршрутизации. - Колокация по умолчанию является безопасной, поскольку маршрут становится общедоступным только после добавления файла
page.jsилиroute.js. Компоненты, тесты и утилиты могут находиться непосредственно внутри сегментов маршрутов, не превращаясь в маршруты, поэтому для этого не требуются частные папки.
Next.js ясно указывает, что не имеет предпочтений относительно способа организации и размещения файлов проекта. В его документации представлены три стратегии организации без их ранжирования.
Как выбрать
Вот решение в сжатом виде с наиболее важными аргументами:
- Менее примерно 30 файлов, или вы ещё изучаете проблему: оставайтесь с простой структурой. Структура, основанная на реальных закономерностях изменений, лучше той, что предполагается с самого начала, как и указывались в документации React.
- Менее примерно 50 файлов, небольшая команда и короткий срок использования: типо-ориентированный подход подойдёт. Он устарел, но это не имеет значения; миграция обойдётся дороже, чем принесёт пользы.
- Любая реальная и долговечная система: используйте подход, основанный на функциях. Это должно быть стандартом. Он соответствует принципам Screaming Architecture, учитывает колокацию компонентов, позволяет легко удалять функции и поддерживает границы, заданные инструментами lint. Начните с одиннадцати папок из проекта Bulletproof React, заранее добавьте зоны с
import/no-restricted-paths, и пропустите файлы типа barrel.
apps/ и packages/, управляемый Turborepo или Nx, позволяя каждому приложению самостоятельно выбирать свою внутреннюю структуру из этого списка.Есть одно правило, которое имеет приоритет над всеми остальными: подходы можно комбинировать. Реальные кодовые базы смешивают их, и в документации React об этом также говорилось, отмечаясь, что в более крупных проектах на практике часто используется смесь подходов, поэтому выбор «правильного» из них с самого начала не так уж и важен.
Совершенно разумная производственная конфигурация — это монорепозиторий, содержащий приложения, ориентированные на конкретные функции, пакет с системой дизайна, организованный примерно по атомарным блокам, и слой, отвечающий за бизнес-логику, в той области, где находятся реальные правила бизнеса. Никто не оценивает вашу архитектуру по степени её чистоты.
Основные выводы
Люди, находящиеся в ближайшем контакте с этими стандартами, относятся к ним гибко и постоянно вносят изменения публично. Фрост называет Atomic Design мыслительной моделью, а не догмой. В документации React время принятия решения было ограничено пятью минутами, после чего страница была удалена. Next.js и Vite отказываются занимать какую-либо позицию. FSD изменил свою основную единицу разбиения на компоненты в версии v2.1, а Bulletproof React изменил свои рекомендации относительно использования файлов в формате barrel-file. Они изменили свой подход, потому что прислушивались к реальным отзывам, а не пытались защитить свою позицию.
Это оставляет два устойчивых правила, оба более старые, чем любые лейауты, рассматриваемые здесь:
- Screaming Architecture: папки верхнего уровня должны объяснять новому разработчику, что делает продукт, а не какую фреймворк-среду он использует.
- Colocation: элементы, которые меняются вместе, должны находиться как можно ближе друг к другу.
Каждая структура в этом руководстве стремится соблюдать эти два правила в определенном масштабе. Выбирайте самый дешевый вариант, который соответствует им для вашего текущего приложения, а не тот, который вы планируете использовать в будущем. Сигналы о необходимости смены подхода явны: префиксы имен файлов вместо папок, четыре открытые папки для одной изменения или потерянный час на обсуждение того, является ли что-то молекулой. Действуйте по этим сигналам, когда они появляются, а не раньше.