
Что важно фронтенд-разработчику при создании веб-приложений? Поддержка текущей кодовой базы, удобство внедрения новых фич и возможность повторно использовать компоненты. Создать такие условия помогает популярный подход к проектированию — FSD (Feature Sliced Design). Разбиваем интерфейс на независимые, переиспользуемые модули (виджеты, фичи и т. д.), получаем чёткие правила, единую структуру проекта и ускорение разработки за счёт переиспользования кода и изоляции ответственности.
Подход FSD во многом прекрасен, но всё же нам в нём не хватало некоторых важных аспектов: внятного разделения слоёв бизнес-логики, удобства работы с кастомными хуками (они быстро разрастаются, обрастают связями и становятся сложными для тестирования). Также было неясно, куда выносить сложные общие компоненты из разных частей проекта. И, например, как легко отделять один бизнес-модуль от другого, не ломая всю систему…Меня зовут Иван Соснович, я тимлид фронтенд-разработки в СберТехе, тружусь в команде Platform V Kintsugi — это графический инструмент для сопровождения, мониторинга и диагностики Postgres-like СУБД. В этой статье я покажу, как мы доработали FSD под себя, и дам ссылку на пример со структурой приложения. Надеюсь, будет полезно фронтенд-разработчикам.Методология FSD позволяет организовать структурированный подход к разработке ПО. Что можно сделать с её помощью?
- Сделать архитектуру понятнее. Код разбивают на независимые модули, что обеспечивает логичную структуру и удобство навигации по проекту. Например, модули авторизации, профиля и других функций отделены друг от друга, что значительно улучшает читаемость и понимание кода.
- Повысить поддерживаемость кода. Каждый модуль ограничен своей зоной ответственности. Проще вносить изменения и исправления. Работа над отдельной функциональностью не нарушает работу остальных частей системы.
- Можно переиспользовать код. Модули и логика могут свободно использоваться в различных частях приложения без дублирования, что снижает затраты ресурсов и повышает эффективность разработки.
- Улучшить масштабируемость. Новые фичи легко интегрируются в систему как отдельные модули, не нарушая существующую архитектуру.
- Тестировать становится удобнее. Чётко очерченные границы модулей облегчают написание и поддержку тестов.
Слои в рамках подхода FSD
1. App.
- Назначение: инициализация приложения.
- Содержимое: включает глобальные настройки (например, темы), роутинг и провайдеры контекста.
- Примеры файлов:
,App.tsx.AppRouter.tsx
2. Entities.
- Назначение: хранение бизнес-сущностей и основной логики работы приложения.
- Содержимое: содержат определения сущностей (например,
,User) и бизнес-логику, связанную с ними.Product - Примеры файлов:
,entities/User.entities/Product
3. Features.
- Назначение: реализация конкретных пользовательских действий.
- Содержимое: компоненты, хуки и логика, которая обеспечивает выполнение задач пользователями (авторизация, добавление товаров в корзину и др.).
- Примеры файлов:
,features/Login.features/AddToCart
4. Shared.
- Назначение: общие утилиты, типы и компоненты, используемые в разных частях приложения.
- Содержимое: переиспользуемые компоненты (например, кнопки), утилиты и глобальные типы.
- Примеры файлов:
,shared/Button,shared/hooks.shared/utils
5. Pages.
- Назначение: сборка всех компонентов для формирования страниц приложения.
- Содержимое: страницы, использующие компоненты из слоёв
,FeaturesиEntities.Shared - Примеры файлов:
,pages/HomePage.pages/ProductPage
6. Widgets.
- Назначение: повторяющиеся крупные блоки интерфейса, которые можно использовать многократно.
- Содержимое: логика и UI-компоненты (например, новости, карусели).
- Примеры файлов:
,widgets/NewsCarousel.widgets/UserProfile
7. 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/ - хуки привязанные к логике проекта, для переиспользования в разных частях
Что у нас получилось по слоям и их функциональности:
1. App.
- Назначение: слой для инициализации приложения.
- Содержит: роутинг, провайдеры контекста/store, хуки настроек, хуки первого рендера и так далее.
2.Entities.
- Назначение: здесь хранятся бизнес-сущности — основные модели и их логика, без UI сущностей.
- Содержит: определения сущностей имеет модульный подход для быстрого отделения их в другой проект.
3. Features.
- Назначение: модули, которые реализуют конкретные пользовательские действия.
- Содержит: только UI-сущности, которые сами решают, в каком виде появиться. Объединяют в себе компоненты из слоёв shared и composition, а также логику из слоя entities.
4. Shared.
- Назначение: общий слой, содержит тему проекта и то, что может быть использовано в другом проекте.
- Содержит: переиспользуемые компоненты (например, кнопки), утилиты, глобальные типы.
5.Pages.
- Назначение: собирает все компоненты, чтобы сформировать страницы приложения.
- Содержит: страницы, которые используют только widgets-компоненты. С редким исключением — логику из Entities.
6.Widgets.
- Назначение: крупные, повторяющиеся блоки, которые можно переиспользовать на разных страницах или только на одной.
- Содержит: модули с логикой и UI (например, блоки новостей, карусели).
7. Composition. Назначение: общий слой всего проекта, для возможного использования во всех слоях
- settings — глобальные настройки приложения;
- components — UI-компоненты для общего использования.
Выбор менеджера состояний
Да, для управления состоянием приложения мы выбрали библиотеку Zustand. Её преимущества: изоляция состояний, простота интеграции с компонентами, высокая производительность и лёгкость тестирования, минимум внешних зависимостей и возможность вызова одного экшена внутри другого.
Результат
Собрали обратную связь у разработчиков: говорят, что с MSD стало проще тестировать код. При доработке функциональности проще дополнять чем-то новым и тестировать реализации. А при работе в большой команде меньше конфликтов в pull request'ах.В общем, мы довольны нашими преобразованиями. И есть планы на будущее. Например, хотим реализовать расширения на VsCode, чтобы быстрее и удобнее работать со структурой. Было бы интересно детальнее разобрать каждый слой с учётом потребностей нескольких приложений. И ещё проверить гипотезу простого разделения на микросервисы.
Выложил здесь пример структуры приложения. Там структура проекта, распределение по слоям, а также связи между слоями и их содержание. Буду рад, если пригодится. А здесь наше сообщество, где мы время от времени выкладываем вакансии и пишем про разработку и всё, что с ней связано.
Спасибо за внимание!
