
Что важно фронтенд-разработчику при создании веб-приложений? Поддержка текущей кодовой базы, удобство внедрения новых фич и возможность повторно использовать компонент��. Создать такие условия помогает популярный подход к проектированию — 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, чтобы быстрее и удобнее работать со структурой. Было бы интересно детальнее разобрать каждый слой с учётом потребностей нескольких приложений. И ещё проверить гипотезу простого разделения на микросервисы.
Выложил здесь пример структуры приложения. Там структура проекта, распределение по слоям, а также связи между слоями и их содержание. Буду рад, если пригодится. А здесь наше сообщество, где мы время от времени выкладываем вакансии и пишем про разработку и всё, что с ней связано.
Спасибо за внимание! Если есть вопросы, предложения, идеи, приглашаю писать в комментарии.
