Всем привет, меня зовут Сергей Сибара, я фронтенд-разработчик в ИТ-холдинге Т1. Так как при использовании Feature-Sliced Design (FSD) возникает много вопросов и разные люди понимают её по-разному, я решил написать статью-справочник, раскрывающий некоторые подробности методологии. В этой статье я продолжаю использовать те же принципы и часть терминологии, что и в предыдущей.
Здесь я, в основном, описываю структурирование по рекомендациям методологии. А в следующей статье, напротив, рассмотрю, как можно улучшить структуру проекта, намеренно нарушая рекомендации FSD. Заранее предупрежу, что правила методологии носят рекомендательный, а не обязательный характер. Их назначение — задать направление структурирования, а дальше принимать решения нужно в зависимости от конкретного проекта и ситуации в нём. Строгое же следование рекомендациям может привести к бо̒льшим проблем, чем их нарушение.
Если заметите ошибки — пишите в комментариях!
Содержание
-
Слой shared
1.1 Как избежать свалки в shared/lib?
1.2 Направление зависимостей в слое shared
1.3 Может ли store (слайс Redux store в случае Redux Toolkit) находиться в слое shared?
1.4 В слое shared стоит размещать только то, что можно переиспользовать в других проектах?
-
Сегменты
2.1 Ошибки группирования файлов в сегментах
2.2 Может ли логика интерфейса, константы, хуки и типы находиться в сегменте ui?
2.3 Когда для сегмента создавать папку, а когда файл?
2.4 Можно ли создавать в сегментах публичный API (файлы index.ts)?
-
Слайсы и связь между ними
3.1 Может ли слайс в entities содержать несколько бизнес-сущностей?
3.3 Всегда ли нужно создавать публичный API (файлы index.ts в слайсах)?
3.4 Могут ли вложенные слайсы и сегменты быть на одном уровне?
-
Слои
4.2 Где разместить функциональность запросов к эндпоинтам и DTO-типы?
4.3 Где размещать мапперы в DTO и во View, переводы, схемы валидации, layout-ы страниц?
1. Слой shared
1.1. Как избежать свалки в shared/lib?
Иногда в shared/lib создают папки вроде hooks, consts, types и т. д., что является неправильным подходом. В документации описано, что это ведёт к группированию файлов по их типу, а не по решаемым ими задачам, а это противоречит принципам FSD. Это создаёт сложности, поскольку каждый файл предназначен для решения несвязанных задач, и их, скорее всего, не получится объединить в подсистему.
Более правильным группированием будет создание в shared/lib папок, таких как:
? validation
? modals
? notifications
? localization
? urlUtils
Например, shared/lib/notifications содержит утилиты, хуки, компоненты интерфейса, константы и типы, реализующие механизм уведомлений, используемый в различных частях приложения. Может показаться, что компоненты этой подсистемы следует располагать отдельно в shared/ui, но в таком случае сильно связанные файлы будет разделены. Уточню, что здесь речь не о том, в какой папке размещать механизм уведомлений, он взят в качестве примера. Речь о том, что файлы подобных подсистем зачастую лучше держать рядом.
Может получиться так, что в shared/lib скопится много инфраструктурных подсистем и различных наборов утилит, дополняющих API JavaScript, браузера и сторонних библиотек, и хранить их все в одной папке станет неудобно. FSD не даёт ответа, что делать в таком случае.
1.2. Направление зависимостей в слое shared
Хаотичные связи в этом слое, как и в других слоях, приведут к запутанной структуре. Поэтому желательно контролировать связи между сегментами и их направление. Например, чтобы сегмент ui импортировал файлы из сегмента lib, но не наоборот.
1.3. Может ли store (слайс Redux store в случае Redux Toolkit) находиться в слое shared?
Да, может.
Если хранилища предназначены для работы с бизнес-сущностями, то им не место в слое shared. Однако, если они предназначены исключительно для работы с инфраструктурной логикой, или являются абстрактными и могут работать с произвольными типами без зависимости от бизнес-логики, то такие хранилища вполне можно разместить в слое shared.
1.4. В слое shared стоит размещать только то, что можно переиспользовать в других проектах?
Если не ошибаюсь, то в прошлом в методологии так и рекомендовалось. Сейчас же такого ограничения нет: в shared разрешается размещать API, маршрутизацию, авторизацию, переводы, темизацию и прочую инфраструктурную функциональность. Перечисленная функциональность (за исключением API) не относится к специфике приложения/бизнеса. Она нужна для работы приложения и для удобства работы с приложением, но не для решения каких-либо задач пользователя или бизнеса.
2. Сегменты
2.1. Ошибки группирования файлов в сегментах
Неправильно создавать сегменты вроде hooks, types, consts. Об этом написано в документации. В предыдущей статья я уже писал, что назначение стандартных сегментов в бизнес-слоях, за исключением сегментов lib и config, такое же, как и у паттернов MVC, MVP, MVVM, Flux и т. д.: группирование кода приложения по техническому назначению. В отличие от подобных паттернов, в FSD предлагается группировать такой код ещё и на уровне файловой структуры проекта.
2.2. Может ли логика интерфейса, константы, хуки, типы находиться в сегменте ui?
Может. Неправильно ограничивать сегмент ui только компонентами.
В бизнес-слоях FSD он является аналогом слоя представления в таких паттернах, как MVC, MVP, MVVM, Flux и т. д. В нём могут содержаться хуки, типы (обычно это props, но могут быть типы для других функций и объектов), константы, различная логика интерфейса и утилиты, относящиеся именно к сегменту ui и используемые только в нём. Если же вынести эти файлы в другие сегменты, то получится, что сильно связанный код станет разделён, а это противоречит принципам Low Coupling и High Cohesion.
2.3. Когда для сегмента создавать папку, а когда файл?
Согласно методологии FSD, если в слайсе каждый сегмент содержит не более одного файла, то дополнительная вложенность в виде папок для группирования сегментов не требуется. Однако, как только хотя бы в одном сегменте появляется два или более файлов, для него следует создать отдельную папку. При этом можно сочетать сегменты-файлы и сегменты-папки в рамках одного слайса.
Рекомендация создавать папку-сегмент, если в сегменте более одного файла, может быть неудобным в случае слайсов с небольшим количеством файлов. А кто-то предпочитает сразу делать сегменты папками, аргументируя тем, что потом слайс может разрастись и придётся переносить файлы сегментов в папки. Но если это заранее неизвестно, то зачем усложнять без необходимости? О том, что не нужно усложнять структуру папок без необходимости, довольно хорошо написано в документации к другой методологии.
2.4. Можно ли создавать публичный API (файлы index.ts) в сегментах?
Это допустимо, но в подавляющем большинстве случаев избыточно. Файлы index.ts предназначены для предоставления доступа только к публичным файлам слайса.
Но думаю, что в редких ситуациях с большим количеством файлов в сегментах слайса всё-таки может быть польза от создания файлов index.ts для экспорта в другие сегменты этого же слайса. В таком случае использование публичного API может помочь избежать путаницы в связях между файлами разных сегментов и ограничить импорт приватных файлов в соседние сегменты.
3. Слайсы и связь между ними
3.1. Может ли слайс в entities содержать несколько бизнес-сущностей?
Да, некоторые бизнес-сущности могут быть сильно связаны друг с другом или быть вложенными. Правильнее размещать такие сущности в одном слайсе, чем пытаться искусственно разделить их.
3.2. Стоит ли создавать для каждой сущности слайс с одним и тем же именем в каждом из слоёв: entities, features, widgets, pages?
Если у вас группы слайсов, то, возможно, сто̒ит, но обычно — нет. Если для каждой сущности (например, product) в каждом или почти в каждом слое создаётся папка с именем этой сущности, например:
entities/products/
features/products/
widgets/products/
pages/products/
то, возможно, вы неправильно поняли, по какому принципу в FSD создаются слайсы.
Только слайс из слоя entities привязан к одной или нескольким связанным бизнес-сущностям. А слайсы в слоях features, widgets и pages обычно не привязаны к определённой бизнес-сущности. Какая-нибудь фича может быть общей для группы сущностей или вообще для всех. Какой-нибудь виджет может работать одновременно с несколькими сущностями. А, например, на странице Dashboard может использоваться десяток сущностей.
Зачастую стоит выделять слайсы, относящиеся к определённой функциональности, а не сущности. Например, слайс в слое features, реализующий механизм фильтрации и импортирующий файлы из нескольких слайсов слоя entities.
Если же вы не переиспользуете фичи и виджеты определённой сущности в нескольких слайсах на уровнях выше, и не планируете это делать, то обычно лучше поместить их в папку pages, как это рекомендуется в подходе pages-first.
3.3. Всегда ли нужно создавать публичный API (файлы index.ts в слайсах)?
Нет, это зависит от особенностей проекта, требований к разделению на чанки и наличия проблем с циклическими зависимостями.
Публичные API полезны в больших проектах, так как они уменьшают количество точек взаимодействия между разными подсистемами. Легче разобраться в связях между подсистемами, чем между отдельными файлами.
Не стоит использовать импортирование из публичного API внутри самого слайса, поскольку это может привести к возникновению циклических зависимостей.
3.4. Могут ли вложенные слайсы и сегменты быть на одном уровне?
Согласно принципам FSD — нет: слайсы и сегменты принадлежат к разным уровням организационной иерархии данной методологии и не должны смешиваться.
3.5. Что делать, когда сущности связаны?
Возможны разные ситуации и варианты решения:
Их можно рассматривать как один слайс (без вложенных слайсов) и не делить на отдельные слайсы. То есть во всех слоях считать их одной сущностью, состоящей из нескольких.
Несколько связанных слайсов можно сгруппировать в один большой слайс с вложенными слайсами.
Использовать кросс-импорты в рамках одного слоя с использованием публичного API и @x-нотации.
Варианты с нарушением FSD в зависимости от ситуации, но об этом в следующей статье.
3.6. @x-нотация для публичного API
В FSD с недавнего времени стало допустимо использовать «правильные кросс-импорты» через дополнительные публичные API. Для этого предлагается использовать @x-нотацию и только в слое Entities. Причины её появления описаны тут.
Кому-то такая нотация может показаться неудобной. Это лишь одна из рекомендаций методологии, а не призыв к действию. Можно отступить от неё и использовать другой подход.
4. Слои
4.1. Стоит ли вводить дополнительные промежуточные слои, которых нет в FSD, чтобы избежать кросс-импортов?
Не рекомендуется вводить дополнительные слои. В FSD и без того довольно много слоёв, и даже стандартных рекомендуется использовать как можно меньше. Рассмотрите альтернативы: объединение связанных слайсов, передача через контекст или props, добавление нескольких кросс-импортов, и т. д.
4.2. Где разместить функциональность запросов к эндпоинтам и DTO-типы?
Ответ зависит от конкретного проекта. В одних случаях оптимальным вариантом будет размещение в слое shared, в других — в бизнес-слоях. При этом инфраструктурный код API размещают в shared/api.
Насколько я понял, в FSD обычно принято использовать следующие варианты:
в shared/api/ — если API генерируется или если в нём много связей между разными бизнес-сущностями;
в entities/{имя_слайса}/api/ — при использовании FSD ниже версии 2.1 для проектов, где редки связи между API разных сущностей;
В сегменте api в том же бизнес-слайсе, где вызывается API-запрос — когда проект ориентирован на pages-first из FSD 2.1 и редки связи между API разных сущностей.
В документации подробно описаны:
4.3. Где размещать мапперы в DTO и во View, переводы, схемы валидации, layout-ы страниц?
Многое уже описано в документации по типам. Про расположение схем валидации написано в пункте «Схемы валидации типов и Zod». Про расположение layout-ов в разных ситуациях написано здесь.
Мапперы из DTO вo View
Официальная рекомендация: размещать рядом с DTO. Я с ней не согласен. Для одного DTO может потребоваться несколько мапперов для разных элементов интерфейса, страниц. Если же в проекте весь API находится в слое shared, то в нём окажутся ещё и типы бизнес-блоков интерфейса. В будущем при неаккуратном удалении ненужных компонентов интерфейса они могут там и остаться. На мой взгляд, мапперы правильнее размещать в слайсе, где находится компонент страницы, для отображения в котором делается преобразование.
Насчёт того, в каком сегменте располагать, я считаю, из стандартных сегментов наиболее подходит сегмент model.
Мапперы из View в DTO
Видел, что в официальном чате обсуждался вариант располагать ближе к формам — в сегменте ui, а также обсуждался вариант располагать в сегменте model. На мой взгляд, второй вариант предпочтительнее, да и в таком случае обе разновидности мапперов будут рядом. Работа с данными, в том числе их преобразование, не является ответственностью слоя представления (слоя View в паттернах MV* или его аналога в FSD — сегмента ui).
Маршрутизация
В последние годы я работал только с React Router, поэтому рассматриваю относительно него.
Маршруты, использующие компоненты-страницы, нужны только в слое app для их инициализации в приложении, поэтому там им и место. Либо, если в слайсах слоя pages несколько страниц, то маршруты можно располагать в соответствующих слайсах и импортировать оттуда в слой app.
А вот константы URL-путей могут понадобиться в любом бизнес-слое, поэтому их зачастую помещают в слой shared, например в shared/routes.
Константы переводов
Глобальные переводы обычно хранят в слое shared — например, в shared/i18n. Если же переводы располагаются в бизнес-слайсах, то для такого случая чаще всего в официальном чате я встречал вариант размещения переводов в сегменте i18n в том же слайсе, что и компонент интерфейса.
Заключение
В статье я собрал основные моменты, вызывавшие вопросы и разногласия в проектах с FSD, в которых мне довелось работать.
Конечно же, были ситуации, когда у команды возникал вопрос, к чему относится компонент: к features или entities. Я не стал его рассматривать, потому что нет универсального понятия, «чем считается» тот или иной элемент интерфейса в FSD. К тому же с выходом FSD 2.1 понятие «фича» изменилось.
В следующей статье я рассмотрю ещё несколько рекомендаций методологии, но уже более спорных.
dominus_augustus
Если методология требует ответа на столько вопросов, может методология говно, не?