![](https://habrastorage.org/getpro/habr/upload_files/8d1/d25/cce/8d1d25ccedd480cf65596d38e2185060.jpg)
Всем привет! Меня зовут Дмитрий, я Frontend-разработчик в VK. В этой статье расскажу немного о том, как мы знакомились с архитектурой FSD (Feature-Sliced Design), как мы рефакторили свой проект под неё. И, самое главное, что из этого вышло. Постараюсь заинтересовать вас, чтобы и вы смело её внедряли в свои проекты. FSD — это, пожалуй, то, чего так не хватало в Frontend-мире.
Для тех, кто еще не слышал о FSD. Это не библиотека, не фреймворк. Это архитектурная методология. То есть свод некоторых правил и советов, как грамотно оформить код в приложении, чтобы потом не запутаться в нем и не сломать ничего при внесении очередных правок.
Главная цель FSD — упорядочить и структурировать код от простого к сложному.
Более подробно останавливаться на описании самой методологии не буду, потому что у меня не получится рассказать лучше, чем это удалось в документации по FSD. Да и раздувать статью пересказом не хочется. Поэтому, перед прочтением этой статьи, рекомендую ознакомиться с докой.
Исходные данные
Итак, мы начали переезд на FSD с одного из наших проектов. Это простой (пока что) личный кабинет для управления процессами. Помимо специальных разделов в нем есть довольно типовой — управление пользователями. Есть и страницы списков, формы и редактирования ролей и приглашения пользователя — все что нужно для полноценного примера.
Используемый стек популярный, проверенный, изученный вдоль и поперек, надёжный, как швейцарские часы:
TypeScript
React
Next.js — роутинг
Redux-toolkit — стейт менеджмент
axios — API-клиент
formik + yup для форм
react-table — для таблиц
VKUI — ui-framework
Структура проекта примерно такова:
![](https://habrastorage.org/getpro/habr/upload_files/fbc/4f6/eda/fbc4f6edaccaabc424ab088185f922fe.png)
У нас изначально было разделение согласно бизнес-логике приложения, что упростило переезд на FSD. Основная рабочая директория — это features/project. Её структура фрактальна и зеркальна навигации приложения. Список пользователей находится тут:
features/project/credentials/users
А форма редактирования пользователя лежит во вложении к списку:
features/project/credentials/users/edit
Каждая такая фича состоит из "суб"-фич и более-менее стандартного набора модулей, назначение которых должно быть понятно без особых пояснений.
В редких случаях, при необходимости, те модели и прочие модули, которые используются в других разделах выносятся в features/common. Сюда же попадают различные виджеты, вроде шапки и подвала страницы, меню и т.д.
По некоторым причинам в features/common не попал UIkit приложения — логика в основном была в том, чтобы можно было бы этот UIkit легко скопировать в другой проект.
По другим причинам в ту же feature-common не попала и свалка helper'ов (папка utils) — туда были сосланы слишком абстрактные методы и слишком мелкие функции, которые нельзя назвать полноценными фичами.Выделены были в отдельные абстракции:
API (/network);
store — тут корневой combineReducer и инициализация RootState;
layouts — Layout'ы страниц NextJS;
styles — некоторые абстрактные глобальные стили, в основном «reset».
Как можно понять, уже с самых первых разделов, наше приложение начало обрастать причудливыми надстройками, пристройками, аппендиксами:
Да, пока в проекте трудится небольшая команда, да и масштаб приложения еще мал, следить за этим обилием директорий и поддерживать в них порядок было довольно просто. Но все изменится, когда связность приложения начнет расти и все больше моделей будет мигрировать в features/common, превращая её в свалку. Или когда придут новые разработчики и не поняв замысла "архитектора" начнут лепить что-то своё.
Недостатки были очевидны:
отсутствие формализованной методологии
отсутствие единого способа деления модулей и четких границ деления кодовый базы — что-то в utils, что-то в ui а что-то в features/common.
слабая масштабируемость приложения.
частичное дублирование структуры pages (Next.js) и features/project, которое было по архитектурным причинам только именно частичное, и это несколько путало.
растущая связность приложения.
Все эти проблемы решились с переездом на архитектуру FSD. Но любой рефакторинг не обходится без подводных камней и в нашем случае всплывать они стали с самого начала.
Next.js vs FSD
В плане связки Next.js + FSD существует сразу несколько проблем. Как общих, так и весьма специфичных. Кто-то в интернетах даже высказывал мнение, что Next.js это не про FSD. Но на самом деле их можно подружить.
Первый камень — что делать с директорией pages? По FSD она должна иметь стандартный вид — слайсы и сегменты. Для последнего pages — это зарезервированное имя и структура этой папки должна соответствовать структуре страниц приложения. Это одна из ключевых механик роутинга в Next.js. Вариантов решения несколько, но мы пошли по самому безболезненному и на наш взгляд оптимальному: мы просто перенесли всю кодовую базу внутрь директории src, а все что касается Next.js оставили в корне проекта.
Помимо прочего, такой шаг нам позволил решить и другую проблему — куда девать Layout'ы страниц?
Доки FSD прямым текстом говорят, что страничная шапка и главное меню — это слой Widget. А страницы — это слой pages. Это два соседних слоя. У нас не остается "места" для композиции виджетов в шаблон страницы.
Варианты были разные, но в контексте Next.js логичнее и удобнее оставить Layout в слое страниц. Да, согласен, это не очень вяжется со здравым смыслом. Но мы сгруппировали эти слайсы в одной директории (что FSD разрешает и поощряет) и они там никому не мешают и нам не пришлось прибегать к кросс-импортам. А саму композицию шаблонов со страницами, как понимаете, мы делаем механизмами Next.js, которые вынесены за пределы FSD.
![](https://habrastorage.org/getpro/habr/upload_files/e96/98a/409/e9698a40994050ca2aceeda38b926fd2.png)
Третий бонус от такого решения — низкая связность с самим Next.js. Да, при желании можно будет вообще от него отказаться и почти безболезненно переехать на другие рельсы, заменив только модули роутинга и i18n.
Два других способа поженить Next.js с FSD — переименовать слой pages во что-то другое или формировать его по структуре Next.js, но и в том и другом случае мы ломаем идеологию FSD.
Слой Shared: что это и с чем его едят
Вторым делом мы занялись наполнением слоя shared. Казалось бы это самый простой слой, но правки в нем влияют на весь проект.
В него улетели сходу и UIkit и utils и даже некоторая часть /features/common (те модули, что не относились к бизнес-логике, например Нотификация, Глобальный Лоадер). Вслед за ними были перенесены и такие модули, как API, и ролевая модель. И да, по поводу последних двух были сомнения, потому что “бизнесу” не место в shared. Но в этом споре выигрывает довод в пользу удобства.
Но что бы минимизировать последствия этого отступления от правил, мы вынесли в API только саму механику обращения к backend, создав таким образом интерфейс получения данных и не более того.
Вторая сложность со слоем shared заключается в том, чтобы не превратить его в помойку. В нем не действуют запреты на кросс-импорты, кто-то предлагает считать, что в нем нет слайсов, третьи заявляют, что в нем надо соблюдать аналогичную иерархию между сегментами, как в слоях: config -> api -> lib -> model -> ui. Вся эта условность и отсутствие внятных правил развязывает руки разработчикам и подталкивает к бессистемности.
Совет: старайтесь этого избегать и выработайте свод своих правил, который бы подходил под ваше приложение, также правила должны автоматически валидироваться плагином для eslint'а, который проверяет кросс-импорты.
App. Первые костыли
За shared последовал, наоборот самый верхний по иерархии FSD-слой — app
. Здесь расположились, генерация Store, различные провайдеры, в том числе VKUI и, собственно, общая инициализация приложения. И тут пришлось решать вторую серьезную дилемму. Как быть с Redux и другими похожими глобальными контекстами? Поясню — композиция глобального Стейта происходит на самом верхнем уровне, в слое app
. Иначе никак. Мы буквально должны собрать все редьюсеры, где они ни были, в слое entity
, feature
, widget
или pages
, а обращаться к этим слоям можно только из слоя app
. При инициализации Store
мы получим наш заветный тип RootState
который надо использовать в дженерик‑функциях useDispatch
и useSelector
, но к этому типу нельзя будет обратиться из нижестоящих слоев по определению FSD.
Чтобы побороть эту проблему пришлось прибегнуть к небольшому костылю, который, впрочем, считается вполне валидным в FSD — объявить RootState
глобальным типом в d.ts
. После чего мы можем объявить наши кастомные, типизированные useAppDispatch
и useAppSelector
в слое shared
и пользоваться ими в любом слое.
declare global {
/**
* ⚠️ FSD
*
* Its hack way to export redux inferring types from @/app
* and use it in @/shared/model/hooks.ts
*/
declare type RootState = import('../src/app/store/reducers').RootState
}
export {}
В остальном слой app
не вызвал никаких сложностей. Он вместе с shared
может формироваться без слайсов, только из сегментов.
![](https://habrastorage.org/getpro/habr/upload_files/059/977/1a7/0599771a7a9bede6c48bb4968aa506bb.png)
Разбиваем на слои
Далее работа пошла уже не послойно, а постранично. Берем одну страницу, в данном случае список пользователей и дробим ее на абстракции разного уровня. Модели, схемы валидации в Entities. А представления же делим на четыре слоя:
![Слой entities Слой entities](https://habrastorage.org/getpro/habr/upload_files/7cb/567/241/7cb567241e04518c484cccd2d848bade.png)
entities
В entities
идут мелкие, совсем атомарные части, например иконка статуса пользователя, аватар и вывод имени. Последний надо представить в виде внешней ссылки на аккаунт VK.
Также стоит понимать, что сущность — это может быть не какой‑то один класс, одна модель. Для примера, имеется два класса — User
и Invite
. Они очень похожи по своей структуре, они частично делят схему валидации, и имеют общее представление. Вариантов решения подобной проблемы есть несколько, но в нашем случае логично было представить это все одной «сущностью», немного пожертвовав низкой связностью.
![Слой features Слой features](https://habrastorage.org/getpro/habr/upload_files/334/2fc/a18/3342fca18dfcb26ee20b56610d8f9402.png)
features
В features
попали средней сложности куски кода. В основном это какие-то действия с сущностями. На странице пользователей у нас их вышло два:
1) Кнопка «Редактировать роли» — простая фича, которая использует глобальный контекст и выполняет одно единственное действие — вызывает модальное окно (widget).
2) «Статус пользователя» — иконка, которая при нажатии меняет статус пользователя, если надо временно отключить его учетку. Эта кнопка посложнее она диспатчит асинхронный Thunk, который отправляет запрос на сервер. Всю эту бизнес‑логику важно разместить именно на этом уровне по двум причинам. Первое — поддержание низкой связности. Второе в интерфейсе слайса мы экспортируем наш Thunk, на который может подписаться другой редьюсер из виджета или страницы. Чем ниже по структуре FSD мы будет располагать эти thunk, тем лучше.
![Слой widget Слой widget](https://habrastorage.org/getpro/habr/upload_files/e44/17f/b75/e4417fb7516d2d4a4c9c9e8802cac7ad.png)
widget
В widget
мы вынесли:
1) список пользователей, только его представление;
2) форму редактирования ролей в модальном окне.
![Слой pages Слой pages](https://habrastorage.org/getpro/habr/upload_files/926/a02/7ee/926a027ee0f2ba3bcda92f612e44d7eb.png)
pages
На слое pages
происходит композиция всего вышеперечисленного, плюс редьюсер, плюс обращение к API за массивом пользователей. Редьюсеры в отличие от Thunk будет удобнее располагать как можно выше, потому что, как Вы знаете, Redux предполагает событийную модель действий — соответственно в диспатч стора должны отправляться события, а не сеттеры, соответственно редьюсер должен подписываться на экшены из фич и виджетов, а не наоборот. В этом плане FSD очень помогает соблюдать один из основных принципов Redux, про который многие новички обычно забывают.
Совет №1: старайтесь вести разработки именно в таком порядке — от сущностей до страниц. Не надо пихать весь код кучей в pages
и потом пытаться это оптимизировать. Наполняйте сначала самые низкие слои, и, если видите, что слайс становится слишком громоздким — это именно тот момент, чтобы задуматься, а не стоит ли декомпозировать ваш код на слой выше.
Совет №2: не гонитесь за тотальным единообразием. Если вы видите, что страница получается слишком простой и вся логика умещается в один слой — разместите её всю в pages
. Не надо декомпозировать ради декомпозирования — вы потом замучаетесь это все поддерживать. Например, у нас есть список сервисов, но в отличие от пользователей это не таблица, а обычные плиточки с иконкой и заголовком. Плитку мы определили в представление сущности, а вывод массива оставили в слое pages
. Мы пропустили в этой цепочке слои виджетов и фич, ибо они были совершенно избыточны. Не бойтесь так делать, если оно оправданно.
Заключение
По итогам нашего рефакторинга мы получили:
Наконец-то формализованные codestyle-правила
Замечательную масштабируемость
Низкую связность кода
Однако, все же стоит держать в уме, что методология все еще разрабатывается и совершенствуется. Это не панацея от всех бед, поэтому отступления от нее и какие-то костыли могут быть весьма вероятны. Очень надеюсь, что моя статья сподвигнет вас на внедрение FSD в свой проект и поможет определиться с решением тех трудностей, с которыми Вы столкнетесь в работе с ней.
Что в планах
Увы, у нас не получилось сходу завести линтеры FSD на проекте, из-за конфликта с flat-конфигами, но мы планируем разобраться и с этой бедой в ближайшем будущем.
Хоть я писал, что не надо устраивать свалку в shared, таки у нас не получилось без небольшого бардачка... В планах также разобраться с этой проблемой и подготовить UIkit к внедрению Story-book на проект.
Комьюнити
На всех этих этапах неоценимой поддержкой был канал от разработчиков методологии. Так отзывчивое, грамотное и дружелюбное комьюнити.
Комментарии (20)
sovaz1997
02.08.2024 13:20+2Это простой (пока что) личный кабинет для управления процессами
В этом-то и проблема. На простом фронте любая
архитектураструктура папок будет выглядеть неплохо.zyets Автор
02.08.2024 13:20Соглашусь)) Но писать сложный проект вообще без методологии еще хуже, наверное
Я участвовал в разных проектах, на React+TS пишу уже 8 лет. По разному пытались строить архитектуру. Лично по моему опыту, FSD пока что лучшее из того, что я пробовал. Как минимум имея уже с десяток страниц в проекте (я в статье не все расписал, само собой), нет ощущения, что что-то не вписывается в эту структуру.
sovaz1997
02.08.2024 13:20Ну хорошо, десяток страниц. А если страниц 1000? Для десятка страниц средней сложности любая структура подойдёт, как мне кажется.
zyets Автор
02.08.2024 13:20+1На мой взгляд число проектов в 10 страниц кратно превышает число проектов в 1000 страниц. Я бы сказал, даже, что немного сомневаюсь, существуют ли такие приложения, на 1000 страниц)) Ну это просто про релевантность проблематики вопроса.
Само собой, на такой сложности проект стоит проектировать что-то свое ибо это вовсе не банальная задача. Но, собственно, и документация FSD об этом говорит, что данная методология вовсе не панацея, но для многих она может быть очень даже полезной. Вы с этим не согласны?
"Отвергаешь - предлагай". Хотелось бы у знать Ваши предпочтения, что бы вы посоветовали, в качестве альтернативы для приложения среднего масштаба. Мы же здесь для обмена опытом и знаниями, не правда ли? ))
sovaz1997
02.08.2024 13:20Да, верно
Вот мне в FSD не понравилось то, что надо задумываться о правильности структуры, о правильности испортив и т. д.
Я бы сказал, что серебряной пули не существует. Где-то вообще может подойти чистая архитектура, но это больше для мобильных приложений, скорее.
А так - всё зависит от проекта. Можно иметь подобие FSD, но, честно говоря, она выглядит переусложненной и, как мне кажется, не очень масштабируемой, так как структура плоская получается и рано или поздно у нас будет очень много слайсов в том или ином месте.
В последнее время предпочитаю фрактальную архитектуру, но и это не панацея)
zyets Автор
02.08.2024 13:20По личному опыту, с импортами не вижу особых проблем, если честно. По началу было боязно, но как говорится, глаза бояться, а руки делают. Пока что было всего пара "узких" места, которые решились довольно легко.
А по поводу обилия слайсов тоже есть выход - fsd допускает их группировку (зря про это не написал в статье, кстати...) Поэтому можете группировать их как вам душе будет угодно и никакой плоскости не будет. Про глубину вложенности группировки я не уточнял, но мне кажется никто вас пинать не будет, если вы решите её делать многоуровневой
BestGodspeed
02.08.2024 13:20+2Офтопик, да. ВК, а вам нормально вместо здоровой конкуренции и развития продукта использовать админ ресурс?
Все хорошо?
zyets Автор
02.08.2024 13:20Прошу прощения, я не понял вопроса.
BestGodspeed
02.08.2024 13:20YT блокируют. ВК делает рекламу, где хвастаются что в отличии от YT у них все быстро грузится. Но ведь это не заслуга ВК. В общем, мерзкое поведение.
olegkusov
Это плохой совет. Оптимизировать структуру нужно там где это требуется. Если не знаете куда кидать компонент, лучше начинать с widget. Иначе будете тратить силы постоянно на размышления о том куда что запихать. FSD оч сильно тормозит скорость разработки если слишком запариваться, а профиты от декомпозиции со временем кажутся не такими очевидными.
/features в FSD по факту это недопапка. Потому что кроме экшен кнопок вы там вряд ли что либо будете хранить. Возьмём например крупную фичу которую бизнес решил добавить в приложение: визуальный редактор аватара пользователя. это фича или виджет?) для бизнеса это точно фича. Но пихать столько всего в папку фичи вы не будете. Тут идёт противоречие с бизнес моделью которое всех будет вводить в замешательство.
zyets Автор
Возможно, Вы и правы. Спасибо за эту ремарку. В конце концов мы и сами только в начале знакомства с FSD.
Мне на самом деле сделали это же замечание буквально вчера, но я решил все же не править статью. Поясню за свои слова: на мой взгляд это все же зависит от специфики коллектива, навыков разработчика и требований бизнеса. При подходе "сверху вниз" на мой взгляд есть шанс скатиться в ситуацию, когда весь код накидан в спешке в page, проверен на работоспособность и отправлен на прод с надеждой в будущем это разобрать и отрефакторить. И вот этот самый рефакторинг не у всех случается. И вся идея FSD идет на смарку.
Если же двигаться снизу вверх, то вы чисто физически не нарушая законов FSD не сможете все разместить в entities. Вам придется что-то поднимать на более верхние уровни так или иначе.
Да, соглашусь, что это может тормозить скорость разработки, но это можно отнести ко многим методологиям. Да даже одним из минусов Typescript считается увеличение времени на разработку, но давайте будем честными, это меньшее из зол.