
Что важно фронтенд-разработчику при создании веб-приложений? Поддержка текущей кодовой базы, удобство внедрения новых фич и возможность повторно использовать компоненты. Создать такие условия помогает популярный подход к проектированию — FSD (Feature Sliced Design). Разбиваем интерфейс на независимые, переиспользуемые модули (виджеты, фичи и т. д.), получаем чёткие правила, единую структуру проекта и ускорение разработки за счёт переиспользования кода и изоляции ответственности.
Подход FSD во многом прекрасен, но всё же нам в нём не хватало некоторых важных аспектов: внятного разделения слоёв бизнес-логики, удобства работы с кастомными хуками (они быстро разрастаются, обрастают связями и становятся сложными для тестирования). Также было неясно, куда выносить сложные общие компоненты из разных частей проекта. И, например, как легко отделять один бизнес-модуль от другого, не ломая всю систему…
Меня зовут Иван Соснович, я тимлид фронтенд-разработки в СберТехе, тружусь в команде Platform V Kintsugi — это графический инструмент для сопровождения, мониторинга и диагностики Postgres-like СУБД. В этой статье я покажу, как мы доработали FSD под себя, и дам ссылку на пример со структурой приложения. Надеюсь, будет полезно фронтенд-разработчикам.
Методология FSD позволяет организовать структурированный подход к разработке ПО. Что можно сделать с её помощью?
Сделать архитектуру понятнее. Код разбивают на независимые модули, что обеспечивает логичную структуру и удобство навигации по проекту. Например, модули авторизации, профиля и других функций отделены друг от друга, что значительно улучшает читаемость и понимание кода.
Повысить поддерживаемость кода. Каждый модуль ограничен своей зоной ответственности. Проще вносить изменения и исправления. Работа над отдельной функциональностью не нарушает работу остальных частей системы.
Можно переиспользовать код. Модули и логика могут свободно использоваться в различных частях приложения без дублирования, что снижает затраты ресурсов и повышает эффективность разработки.
Улучшить масштабируемость. Новые фичи легко интегрируются в систему как отдельные модули, не нарушая существующую архитектуру.
Тестировать становится удобнее. Чётко очерченные границы модулей облегчают написание и поддержку тестов.
Слои в рамках подхода FSD
-
App.
Назначение: инициализация приложения.
Содержимое: включает глобальные настройки (например, темы), роутинг и провайдеры контекста.
Примеры файлов:
App.tsx,AppRouter.tsx.
-
Entities.
Назначение: хранение бизнес-сущностей и основной логики работы приложения.
Содержимое: содержат определения сущностей (например,
User,Product) и бизнес-логику, связанную с ними.Примеры файлов:
entities/User,entities/Product.
-
Features.
Назначение: реализация конкретных пользовательских действий.
Содержимое: компоненты, хуки и логика, которая обеспечивает выполнение задач пользователями (авторизация, добавление товаров в корзину и др.).
Примеры файлов:
features/Login,features/AddToCart.
-
Shared.
Назначение: общие утилиты, типы и компоненты, используемые в разных частях приложения.
Содержимое: переиспользуемые компоненты (например, кнопки), утилиты и глобальные типы.
Примеры файлов:
shared/Button,shared/hooks,shared/utils.
-
Pages.
Назначение: сборка всех компонентов для формирования страниц приложения.
Содержимое: страницы, использующие компоненты из слоёв
Features,EntitiesиShared.Примеры файлов:
pages/HomePage,pages/ProductPage.
-
Widgets.
Назначение: повторяющиеся крупные блоки интерфейса, которые можно использовать многократно.
Содержимое: логика и UI-компоненты (например, новости, карусели).
Примеры файлов:
widgets/NewsCarousel,widgets/UserProfile.
-
Processes (опционально).
Назначение: вынос сложных процессов, объединяющих несколько функциональных возможностей.
Содержимое: бизнес-процессы, такие как оформление заказов.
Примеры файлов:
processes/Checkout.
Всё хорошо, но… Чего нам не хватало в FSD?
Несмотря на очевидные преимущества, в базовом подходе FSD нам не хватало некоторых важных аспектов:
Грамотно расписанных слоёв бизнес-логики.
Гибкости модульности (именно на уровне бизнес-логики).
Кастомные хуки разрастались внутри, обрастали множественными связями, в результате их становилось трудно тестировать.
Не было чёткого понимания, где размещать общие сложные компоненты, используемые в нескольких частях проекта.
И нельзя было легко отделять один бизнес-модуль от другого без нарушения общей функциональности.
Мы решили кастомизировать…
Знакомьтесь, MSD
… и получился свой подход, который назвали MSD (Modules Sliced Design). Буквально можно перевести как «проектирование на основе модульных слайсов (срезов)». Он основан на принципах FSD, но дополнен нашими идеями и решениями:
Основные принципы
Каждый слой (Pages, Widgets, features) имеет одинаковую семантику папок. Слой pages:
├── pages/
│ ├── user/
│ │ ├── create/
│ │ │ ├── ui/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.styled.ts
│ │ │ │ ├── index.types.ts
│ │ │ ├── index.js export { Create as CreateUserPage } from './ui' — заменяем имя страницы при экспорте
│ │ ├── edit/
│ │ │ ├── ui/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.styled.ts
│ │ │ │ ├── index.types.ts
│ │ │ ├── index.js
│ │ ├── settings/
│ │ │ ├── ui/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.styled.ts
│ │ │ │ ├── index.types.ts
│ │ │ ├── index.js
│ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сама страница
src/
│
├── pages/
│ ├── user/
│ │ ├── create/
│ │ │ ├── ui/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.styled.ts
│ │ │ │ ├── index.types.ts
│ │ │ ├── index.js export { Create as CreateUserPage } from './ui' — заменяем имя страницы при экспорте
│ │ ├── edit/
│ │ │ ├── ui/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.styled.ts
│ │ │ │ ├── index.types.ts
│ │ │ ├── index.js
│ │ ├── settings/
│ │ │ ├── ui/
│ │ │ │ ├── index.jsx
│ │ │ │ ├── index.styled.ts
│ │ │ │ ├── index.types.ts
│ │ │ ├── index.js
│ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сама страница
│
├── widgets/
│ ├── user/
│ │ ├── create/
│ │ │ ├── form/
│ │ │ │ ├── config/
│ │ │ │ ├── constants/
│ │ │ │ ├── lib/
│ │ │ │ ├── ui/ — внутри лежат все компоненты для реализации этого функционала
│ │ │ ├── index.js
│ │ │ ├── header/
│ │ │ │ ├── config/
│ │ │ │ ├── constants/
│ │ │ │ ├── lib/
│ │ │ │ ├── ui/
│ │ │ │ ├── index.js
│ │ │ ├── footer/
│ │ │ │ ├── config/
│ │ │ │ ├── constants/
│ │ │ │ ├── lib/
│ │ │ │ ├── ui/
│ │ │ │ ├── index.js
│ │ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сам виджет
│ │ ├── edit/...
│ │ ├── settings/...
│ │ ├── index.js export * from './user' — отдаём во внешний мир всё, что разрешают сами виджеты
|
├── features/
│ ├── user/
│ │ ├── create/
│ │ │ ├── form/
│ │ │ │ ├── config/
│ │ │ │ ├── constants/
│ │ │ │ ├── lib/
│ │ │ │ ├── ui/ внутри лежат все компоненты для реализации этого функционала
│ │ │ ├── index.js
│ │ │ ├── header/
│ │ │ │ ├── config/
│ │ │ │ ├── constants/
│ │ │ │ ├── lib/
│ │ │ │ ├── ui/
│ │ │ │ ├── index.js
│ │ │ ├── footer/
│ │ │ │ ├── config/
│ │ │ │ ├── constants/
│ │ │ │ ├── lib/
│ │ │ │ ├── ui/
│ │ │ │ ├── index.js
│ │ │ ├── index.js export * from './create' — отдаём во внешний мир всё, что разрешает сама фича
│ │ ├── edit/...
│ │ ├── settings/...
│ │ ├── index.js export * from './user' — отдаём во внешний мир всё, что разрешает сам набор фичей.
Слой shared содержит функционал, не привязанный к бизнес-логике, и он должен переноситься в другой проект без каких-либо манипуляций.
├── ui/ — и все простые компоненты проекта не привязаны ни к какой логике проекта
├── api/ — базовая настройка слоя взаимодействия (например, настройка axios)
├── theme/ — провайдер темы библиотеки, без которых не могут существовать ui-компоненты
├── hooks/ — общие хуки приложения (useDebounse, useOutsideClick), именно те хуки, которые могут быть переиспользованы в других проектах
├── lib/ — утилиты (например, копирование значения в буфер, работа с local storage)
├── "@types"/ — декораторы типов
Слой app — для настройки всего проекта. По сути, от FSD отличий нет, кроме того, что убрана тема.
Слой entities — тоже без изменений по сравнению с FSD, но теперь тут нет UI, порядок вложения как и у слоёв Pages:
├── pages/
│ ├── user/
│ │ ├── create/
│ │ ├── DTO/ — все типы для взаимодействия с бэкэндом
│ │ ├── types/ — все типы для внутреннего использования
│ │ ├── lib/ — утилиты, которые требуется использовать внутри данной сущности
│ │ ├── api/ — все необходимые вызовы API для данной сущности
│ │ ├── store/ стор — по сущности, далее по нему будет подробный раздел
│ │ ├── parsers?/ Не все готовы перебирать из типов DTO в типы для использования проекта, так как может появиться множество дублей. Мы решили пока отказаться от этого.
│ │ │ ├── index.js
│ │ ├── edit/...
│ │ │ ├── index.js
│ │ ├── settings/...
│ │ │ ├── index.js
│ │ ├── index.js
И появляется новое — слой composition/ Зачем? Для чего?
├── ui/ — сложные компоненты, которые могут быть переиспользованы в разных местах, но везде должны быть привязаны к одному типу из entities
├── layer/ — слои для формирования расположения компонентов
├── settings/ — настройки проекта, общие для всех. Таймеры, фича-тоглы и так далее.
├── components/ - сложные компоненты проекта для переиспользования (formField, widgets)
├── hooks/ - хуки привязанные к логике проекта, для переиспользования в разных частях
Что у нас получилось по слоям и их функциональности:
-
App.
Назначение: слой для инициализации приложения.
Содержит: роутинг, провайдеры контекста/store, хуки настроек, хуки первого рендера и так далее.
-
Entities.
Назначение: здесь хранятся бизнес-сущности — основные модели и их логика, без UI сущностей.
Содержит: определения сущностей имеет модульный подход для быстрого отделения их в другой проект.
-
Features.
Назначение: модули, которые реализуют конкретные пользовательские действия.
Содержит: только UI-сущности, которые сами решают, в каком виде появиться. Объединяют в себе компоненты из слоёв shared и composition, а также логику из слоя entities.
-
Shared.
Назначение: общий слой, содержит тему проекта и то, что может быть использовано в другом проекте.
Содержит: переиспользуемые компоненты (например, кнопки), утилиты, глобальные типы.
-
Pages.
Назначение: собирает все компоненты, чтобы сформировать страницы приложения.
Содержит: страницы, которые используют только widgets-компоненты. С редким исключением — логику из Entities.
-
Widgets.
Назначение: крупные, повторяющиеся блоки, которые можно переиспользовать на разных страницах или только на одной.
Содержит: модули с логикой и UI (например, блоки новостей, карусели).
-
Composition.
-
Назначение: общий слой всего проекта, для возможного использования во всех слоях:
settings — глобальные настройки приложения;
components — UI-компоненты для общего использования.
-
Выбор менеджера состояний
Да, для управления состоянием приложения мы выбрали библиотеку Zustand. Её преимущества: изоляция состояний, простота интеграции с компонентами, высокая производительность и лёгкость тестирования, минимум внешних зависимостей и возможность вызова одного экшена внутри другого.
Результат
Собрали обратную связь у разработчиков: говорят, что с MSD стало проще тестировать код. При доработке функциональности проще дополнять чем-то новым и тестировать реализации. А при работе в большой команде меньше конфликтов в pull request'ах.
В общем, мы довольны нашими преобразованиями. И есть планы на будущее. Например, хотим реализовать расширения на VsCode, чтобы быстрее и удобнее работать со структурой. Было бы интересно детальнее разобрать каждый слой с учётом потребностей нескольких приложений. И ещё проверить гипотезу простого разделения на микросервисы.
Выложил здесь пример структуры приложения. Там структура проекта, распределение по слоям, а также связи между слоями и их содержание. Буду рад, если пригодится. А здесь наше сообщество, где мы время от времени выкладываем вакансии и пишем про разработку и всё, что с ней связано.
Спасибо за внимание! Если есть вопросы, предложения, идеи, приглашаю писать в комментарии.
nin-jin
NeuroSlopDesign