Добрый день!
Пишу клиентскую часть приложения с использованием reactjs и redux.
После нескольких месяцев работы над проектом накопились вопросы по поводу архитектуры проекта, организации редакса.
Есть следующая структура:
├── src
│ ├── assets
│ ├── components
│ │ ├── App.jsx
│ │ ├── blocks
│ │ ├── common
│ │ ├── settings
│ │ │ ├── components
│ │ │ │ ├── accounts
│ │ │ │ ├── buttonDotted
│ │ │ │ ├── collections
│ │ │ │ ├── settingsFilter
│ │ │ │ └── stores
│ │ │ │ ├── components
│ │ │ │ │ └── store
│ │ │ │ ├── stores.css
│ │ │ │ └── stores.jsx
│ │ │ ├── settings.css
│ │ │ ├── settings.jsx
│ │ │ ├── settingsNavigation.jsx
│ │ │ └── settingsRouter.jsx
│ │ └── signup
│ ├── constants
│ ├── index.js
│ ├── store
│ │ ├── actions
│ │ ├── constants
│ │ ├── reducers
│ │ └── store.js
│ └── util
Сразу оговорюсь что структура проекта естественно не вся и названия компонентов изменены в целях безопасности, любые совпадения случайны))
Продолжим по теме:
Внутри папки
src имеем скелет:
assets - всякая статика(шрифты, картинки, общий css)...
components - код реакта, компоненты как "умные так и глупые";
components/blocks - умные компоненты, которые подключены к стору редакса и используются в более чем одном месте в приложухе;
components/common - глупые компоненты (кнопка, инпут...);
components/settings, components/signup - страницы приложения;
constants - константы для приложения;
store - все добро для редакса;
util- самописные утилиты;
index.js - точка входа.
Подробнее об
src/components.
Страницы (
signup например -
components/signup/signup.jsx) подключены к стору.
Если нужно разделить вьюху страницы от её логики - рядом c
signup.jsx будет лежать
signupComponent.jsx, как это сделано:
----signup
├── components
├── signup.css
├── signup.jsx
└── signupComponent.jsx
В случае выше
signup.jsx будет подключена к стору редакса и будет иметь какую то логику (валидация формы, локальный стейт страницы, хендлеры на разные события типа вызова модалки восстановления пароля и пр.) и пробрасывать все нужное в
signupComponent.jsx, а
signupComponent.jsx - это глупый компонент, который собирает стили, компоненты из папки
signup/components в одну вьюху(страницу), пропускает пропсы дальше в нужные компоненты типа формы, инпутов и пр.
Папка
components внутри страницы?
Да - в каждой папке страницы по необходимости есть папка components в которой будут лежать компоненты специфичные для данной страницы (как умные, так и глупые).
Если посмотреть на структуру папок, то у страницы
settings есть
settingsRouter.jsx, имеем вложенный роутинг внутри settings. Так же внутри
settings/components есть компоненты
accounts, collections, stores - дочерние роуты для settings, которые в свою очередь подключены к редаксу.
Сама
settings.jsx к стору редакса не подключена - в моем случае это просто враппер для компоновки навигации и вьюх дочерних роутов.
На примере
components/settings/components/store можно заметить что там есть еще папка
components с компонентами для
stores, слава богу последняя этом поддереве папка
components.
Такая вложенность папок components/ в каждую страницу имеется по всем страницам и по мере необходимости в папке
blocks - "умных компонентах".
Рабочий процесс с этим добром такой:
Если умный компонент нужно переиспользовать еще на одной странице то он отправляется из папки
components страницы в которой он использовался в папку
src/blocks.
Если есть вьюха которую нужно переиспользовать еще на одной странице - то она отправляется в папку
src/common
С одной стороны вроде понятно сразу что куда и к кому относится. И напоминает чем то
фрактальную структуру.
Проект у нас небольшой, около 10-ти страниц. Учитывая специфику проекта добавлять новые фичи в такую структуру будет не больно.
Но что то тревожит, глазу не совсем приятно. Все время кажется что организация папок/проекта не "идеальная".
Тут подходим к первым вопросам, даже к просьбе выразить рекомендации взглянув на проект Вашим "свежим глазом".
1) На сколько по вашему мнению мой подход отличается от той же фрактальной структуры проекта в худшую/лучшую сторону?
2) Субъективно, удобнее ложить вложенные компоненты в папку components или на одном уровне папки страницы как это предлагается в статье о фрактальной структуре?
3) Чем-то моя структура может грозить в будущем?
4) Если вы так же использовали атомарную архитектуру или имеете мнение на этот счет, то поделитесь плюсами/минусами в сравнении выбранного мною подхода и атомарной архитектуры.
Очень интересно услышать мнения и изучить рекомендации бывалых.
Далее на повестке дня редакс любимый.
Есть директория
src/store в которой включены все actions, reducers, константы для редакса(типы экшнов) и собственно
store.js который редьюсеры собирает и точку входа отдает созданное хранилище:
├── store
│ ├── actions
│ │ ├── signup
│ │ └── store
│ │ └── store.js
│ ├── constants
│ ├── reducers
│ │ ├── index.js
│ │ ├── signup
│ │ │ └── index.js
│ │ └── store
│ │ ├── connectToStore.js
│ │ ├── getStores.js
│ │ ├── store.js
│ │ └── syncStore.js
│ └── store.js
Сначала делал по офф. докам редакса -
src/actions,
src/reducers, константы экшнов в
src/constants. По мере роста приложения стало неудобно прыгать между папками. Объединил все связанное с редаксом в папку
src/store вплоть до отдельной папки констант. Стало значительно удобнее. Под каждый тип действий в
store/actions и в
store/reducers своя папка - тоже удобно, порядочек.
НО! Есть одно жирнющее но - дикое дублирование кода.
Что касается экшнов, хочется что бы красиво, понятно что происходит:
код экшна.
Функция которую мы пробрасываем к нужному компоненту что бы запросить список какой нибудь полезной инфы -
getStoresAction.
Что бы понимать на каком этапе мы находимся, нам нужно 3 экшна для каждой стадии, это:
getStores, getStoresSuccess, getStoresFailure.
Что в редьюсере:
код редьюсера
Тоже удобно, все просто и понятно.
В директории редьюсера лежат файлы с импортами функций которые в редьюсере как раз обрабатывают разные экшны. Как выглядят:
код syncStore и
код getStore
Начав использовать такой подход сразу очевидны плюсы - все под контролем и видно что за что отвечает. Знаем точно когда спиннер загрузки покрутить, когда сообщение об ошибке бросить.
Код начал расти, копипаста тоже. Я успокаивал себя мыслью что такой подход дает полный контроль и в случае чего, я смогу без труда вносить изменения в работу той или иной части приложения не затрагивая другие её части. Но по мере роста приложухи я понял что на копипасту и пересоздания одинаковых экшнов и редьюсеров(с разными именами :) ) я трачу много времени - которого жалко.
Я начал гуглить и все варианты что я находил субъективно были достаточно сложными и предполагали в моем случае переписать около половины проекта - увы не вариант.
Встречал подход, в котором все умные компоненты включали в себя свои экшны, редьюсеры, константы и все что ему нужно, условно его можно было портировать в другой проект - модульная архитектура вроде. Что интересно - там главный редьюсер как то по хитромудрому собирает из всех компонентов редьюсеры и компонует их в один редьюсер подставляя названия экшнов из компонента, что то вроде:
`${store}.FETCH`, `${store}.FETCH_SUCCESS`
и так далее. При этом используя для экшнов с приставкой ".FETCH" один и тот же обработчик, для ".FETCH_SUCCESS" другой. И дублирования кода как у меня - не было. В общем склоняюсь в следующем проекте к такой архитектуре. Но вникать не стал, потому что текущему проекту такой глубокий рефакторинг ни к чему.
Так же у меня были мысли сделать несколько общих функций для редьюсеров и одинаковые места обрабатывать одними и темы же функциями, что уменьшит общее кол-во кода в редьюсерах, но немного уменьшит гибкость.
5) Что вы думаете о модульной структуре? Может есть примеры, опыт которыми можете поделиться?
6) Как вы боретесь с таким дублированием кода в редьюсерах и экшнах как у меня?
7) Исходя из того кода, что я имею сейчас можно ли отделаться малой кровью и избавиться от дублирования кода?
8) Как вы организовываете ваш редакс?
У меня есть экшны которые делают запросы к апи.
Но мне нужно полученные данные изменить. Пришел массив объектов с сервера, а мне в каждый объект нужно добавить пару полей с булевыми значениями(кейс: на одной странице можем выбирать сторы, и на основе выбора будут подгружаться данные, а на другой странице те же сторы мы можем так же выбирать но на основе выбранных сторов будет подгружаться скажем аналитика). Вот и нужно каждому объекту стора добавить два поля: checkedInStoresPage, checkedInAnalyticPage.
9) Вопрос, в каком месте собирать логику которая с данными делает преобразования, в экшне или в редьюсере?
И последнее:
Есть экшны, которые не делают запросы к апи, например из вопроса выше изменения состояния checkedInStoresPage/checkedInAnalyticPage.
Сейчас имею следующий экшн:
код toggleStoreAction
Это работает, это просто и легко.
10) Но можно ли хранить такую логику в экшнах или выносить в редьюсер?
Спасибо вам что уделили время, хорошего дня!