По мере роста объема, размера и набора функций вашего приложения React + Redux растут и трудности с его обслуживанием, особенно когда вы работаете в большой команде, и над каждой функцией в вашем приложении работают несколько человек. В этой статье мы собираемся изучить, как организовать вашу кодовую базу, чтобы повысить ремонтопригодность и расширяемость.
Когда вы впервые начинаете писать новое приложение React, набор функций невелик, а API React прост, но достаточно гибок и достаточно мощный для управления состоянием. Однако со временем ваше приложение становится больше, и недостатки React setState
в управлении состоянием для более крупных приложений становятся очевидными и болезненно.
Итак, вы проводите небольшое исследование и понимаете, что Redux - лучший выбор для удовлетворения растущих потребностей вашего приложения в управлении состоянием. Redux представляет actions
, actionCreators
, reducers
, middleware
и stores
. А поскольку это приложение React, вам необходимо подключить Redux к компонентам React, поэтому вы также захотите использовать React-Redux. Сложите все это, и ваше приложение может быстро превратиться в огромное количество спагетти-кода, если вы не организуете свое приложение должным образом.
Обеспечение того, чтобы ваше приложение было организовано должным образом, в манере, понятной для всей вашей команды, может сэкономить вам много времени и ресурсов при поддержке и расширении приложения. Я писал приложения React в течение последних трех с лишним лет, причем эти приложения различались по сложности от тривиальных до сверхсложных.
В процессе разработки этих приложений я экспериментировал с несколькими способами организации своих приложений React и, думаю, наконец остановился на шаблоне, который обеспечивает наилучший баланс простоты использования и удобства обслуживания. Я называю этот шаблон организации моего приложения шаблоном State-View
. Но прежде мы рассмотрим шаблон State-View
, давайте сначала посмотрим на другие шаблоны, через которые я прошел, и почему я обнаружил, что их не хватает.
Группировка по функциям / типу файла
В приложении React + Redux у нас может быть несколько разных типов файлов, каждый из которых выполняет определенную функцию. Например, у нас есть reducers
, который выводит новое состояние из данного состояния, и action
, actionCreators
, которые создают action
для передачи reducers
, store
, который хранит состояние приложения и позволяет components
подписаться на изменения в store, presentational components
, которые создают представления, отображаемые на экране, container components
, которые выполняют основную часть тяжелой работы и обеспечивают бизнес-логику, постоянство и т. д.
Самый распространенный способ организовать приложение React + Redux - просто сгруппировать файлы по типу / функциям этого файла. Так организовано подавляющее большинство приложений React + Redux. Если вы посмотрите на Реальный пример из репозитория Redux Github, вы увидите, что именно так он устроен.
В реальном примере проекта:
configureStore.dev.js
,configureStore.prod.js
- это дваstores
(только один из которых используется в зависимости от среды выполнения приложения), и они сгруппированы в каталогеstore
.index.js
иpaginate.js
экспортируют несколько редукторов, которые сгруппированы вместе в каталогеreducers
.api.js
предоставляет промежуточное ПО Redux, которое используется для вызовов API Github API и нормализации результатов. Он находится в каталогеmiddleware
.- Различные
container components
сгруппированы вместе в каталогеcontainers
. - Различные
presentational components
сгруппированы вместе в каталогеcomponents
. - Все действия определены в одном
index.js
, который находится в каталогеactions
. - Точка входа приложения
index.js
помещается прямо в корень.
Преимущество организации вашего приложения путем группировки файлов с одной и той же функцией вместе заключается в том, что вы всегда точно знаете, где будет находиться конкретный тип файла. Например: если вы отлаживаете проблему с некорректным переходом между состояниями, вы знаете, что единственное место, где может произойти переход состояния, находится в пределах reducer
, а все reducers
находятся в каталоге reducers
.
Это работает хорошо, пока ваше приложение невелико и содержит только несколько компонентов и связанных actions
, reducers
и middleware
. По мере того, как ваше приложение становится больше, вы обнаружите, что сбрасываете все больше и больше файлов, которые только косвенно связаны друг с другом в одном и том же каталоге, и просмотр этих файлов для выявления конкретных проблем начнет отнимать все больше вашего времени и ресурсов. Это только усугубляется, когда несколько разработчиков работают над одной и той же кодовой базой и все работают с одними и теми же файлами в одном каталоге, чтобы заставить работать свои собственные индивидуальные функции.
Группировка по функциям приложения
Основная проблема подхода «группировка на основе функций / типов» к организации вашей кодовой базы заключается в том, что для работы над одной функцией вам придется работать с файлами, которые распределены по всему проекту в нескольких несвязанных каталогах. Чтобы решить эту проблему, вы можете вместо этого организовать свое приложение React + Redux, сгруппировав вместе файлы, связанные с определенной функцией вашего приложения.
Если изменить тот же пример Redux Real-World для группировки по функциям приложений, он будет выглядеть следующим образом:
Здесь мы разделили наш код на разные «функции»:
Root
- компонент корневого контейнера, который выполняет маршрутизацию и отображает другие компоненты. Поскольку это не поддерживает и не управляет состоянием, в каталоге нет никакихactions
илиreducers
. Мы также переименовали компонент-контейнер и добавили к файлу суффиксContainer
, чтобы обозначить его какcontainer
компонент.App
- Основной компонент приложения. Поскольку это поддерживает состояние, указывающее, был ли вызов API выполнен с ошибкой, в каталоге естьactions.js
иreducers.js
. Вы могли заметить, что мы переименовали компонентExplore
вExploreComponent
. Это означает, что это презентационный компонент.DevTools
- отвечает за DevTools, которые регистрируют состояние при переходе с каждым действием. Не управляет никаким собственным состоянием, поэтому в этом каталоге есть только компоненты.RepoPage
иUserPage
- эти каталоги соответствуют функцииRepoPage
, которая перечисляет отмеченных звездочкой наблюдателей за репо, и функцииUserPage
, которая перечисляет репозитории, которые есть у пользователя. Эти компоненты управляют своим собственным состоянием, в частности, они управляют свойствамиentities
иpagination
объекта состояния приложения. Помните, это скоро станет важным.common
- сюда помещаются любыеcomponent
,middleware
,reducer
илиaction
, которые используются в нескольких функциях.
Это уже выглядит намного лучше, чем «Группировка на основе функции / типа». Если мы хотим работать над определенной функцией, все файлы, связанные с этой функцией, находятся в одном каталоге. Сюда входят actions
, reducers
, presentational components
и container components
. Код, связанный с представлением и состоянием, находится в одном каталоге, что позволяет нам легко вносить изменения для конкретной функции без необходимости идти и изменять пять разных файлов в пяти разных каталогах.
Этот подход обычно хорошо работает для тех приложений, где существует взаимно однозначное соответствие между элементами состояния и представлением. Например, в списке задач каждый элемент задачи всегда отображается только в одном месте на экране через один компонент представления. Таким образом, мы можем четко разделить состояние приложения на основе функции, которая использует это состояние, и мы можем сгруппировать связанные actions
и reducers
вместе с компонентами представления для этой функции.
Но что происходит, когда отображение между элементами состояния и компонентами представления не однозначно. Именно это и происходит в случае с анализируемым нами «реальным примером редукции». В этом приложении функции RepoPage
и UserPage
независимо используют свойства entities
и pagination
объекта состояния. Обе функции читают и записывают в эти свойства, разделяя между собой одни и те же actions
и reducers
.
Как в таком случае сгруппировать состояние и представление вместе? Какие actions
и reducers
следует сгруппировать с RepoPage
, а какие - с UserPage
? Группирование кода, связанного с состоянием, в основном actions
и reducers
с одной функцией, нарушает нашу модель группировки, и теперь неверно, что у нас всегда есть весь код, связанный с определенной функцией, в одном каталоге. Ввод actions
и reducers
в common
также приводит к точно такой же проблеме.
Шаблон представления о состоянии
Недавно я работал над довольно сложным приложением React + Redux, для которого я экспериментировал с новым шаблоном организации кодовой базы моего приложения. Это эволюция подхода «группировка на основе функций» к организации вашего кода. Практически единственная проблема (хотя и серьезная) подхода к группировке на основе функций - это непредсказуемость в отношении состояния, когда состояние не сопоставляется один-к-одному с представлениями.
Чтобы исключить эту непредсказуемость, мы разделили кодовую базу на два отдельных основных каталога, и файлы сгруппированы в каждом каталоге на основе различных факторов. Как вы уже догадались, эти два каталога - это каталоги state
и view
.
Весь код, связанный с управлением состоянием, включая все actions
, reducers
, middleware
и stores
, помещается в каталог state
. Весь остальной код помещается в каталог view
. Так как же дальше организованы эти каталоги?
Давайте сначала посмотрим, как выглядит общая структура приложения, когда реальный пример Redux переписывается с использованием шаблона State-View
:
Есть два основных каталога state
и view
. Давайте сначала исследуем каталог view
.
Root
- компонент корневого контейнера, который выполняет маршрутизацию и отображает другие компоненты. Отсюда удаляется весь код, связанный с управлением состоянием, и сохраняется только конкретный код просмотра. Мы также переименовали компонент-контейнер и добавили к файлу суффиксContainer
, чтобы обозначить его какcontainer
компонент.App
- Основной компонент приложения. Вы могли заметить, что мы переименовали компонентExplore
вExploreComponent
. Это означает, что это презентационный компонент.DevTools
- отвечает за DevTools, которые регистрируют состояние при переходе с каждым действием.RepoPage
иUserPage
- эти каталоги соответствуют функцииRepoPage
, которая перечисляет отмеченных звездочкой наблюдателей за репо, и функцииUserPage
, которая перечисляет репозитории, которые есть у пользователя. Как видите, эти каталоги содержат только компоненты представления, а всеactions
иreducers
были удалены._shared
- Здесь сгруппированы вместе все компоненты вида, которые используются несколькими функциями.
Как видите, каталог view
организован очень аналогично тому, как приложение организовано в шаблоне группировки на основе функций. Причина этого довольно проста. Не было никаких серьезных проблем с тем, как компоненты view
были организованы в шаблоне группировки на основе функций, поэтому мы просто оставили все как есть.
Теперь давайте посмотрим на каталог state
. Именно здесь происходит большинство изменений в шаблоне группировки на основе признаков. Прежде всего, вы заметите, что имена подкаталогов внутри каталога view
совершенно не соответствуют именам подкаталогов внутри каталога state
. Это сделано намеренно и сделано для того, чтобы убедить, что состояние не имеет однозначного сопоставления с представлением.
Вместо того, чтобы упорядочивать содержимое state
каталогов по функциям приложений, мы упорядочиваем их по свойствам хранилища, за которое они отвечают. В реальном примере с редукцией состояние приложения выглядит примерно так:
const state = {
errorMessage: null,
entities: {},
pagination: {}
};
Если мы вернемся в каталог state
, подкаталоги будут соответствовать свойствам состояния. Каждый подкаталог содержит actions
и reducers
, необходимые для управления этой конкретной частью состояния. Все общие элементы помещаются в каталог _shared
. Эти actions
и reducers
затем сворачиваются и объединяются с использованием файлов actions.js
и reducers.js
в каталоге state
, и затем они экспортируются index.js
. Итак, теперь, если нам нужно использовать определенное действие в любом из наших компонентов представления, мы можем потребовать его следующим образом:
import { actions } from '../../state';
const { loadRepo, loadStargazers } = actions;
Если вы хотите взглянуть на код шаблона state-view
для реального приложения-примера redux, исходный код для него доступен на Github.
Таким образом, мы эффективно отделили способ организации кода, связанного с состоянием, от способа организации кода, связанного с представлением. Поскольку состояние больше не группируется вместе с представлениями, у нас снова есть очень предсказуемый шаблон для нашего кода. Любой связанный с представлением код всегда будет в соответствующем подкаталоге функции в каталоге view
. Любые манипуляции с состоянием, выполняемые этими представлениями, будут находиться в подкаталоге с соответствующим именем свойства состояния в каталоге state
. Таким образом, пока вы знаете, где находится ваше представление (что должно быть легко определить на основе функции, частью которой оно является), вы можете легко выяснить, на какое состояние оно влияет и где находится соответствующий код манипулирования состоянием.
Ура! Мы восстановили предсказуемость структуры нашего приложения React + Redux, и это должно позволить нам поддерживать и расширять наше приложение намного проще, чем мы могли бы сделать это раньше.
Я хотел бы услышать ваше мнение о том, что я написал в этой статье. Так что поделитесь своими комментариями ниже.
Первоначально опубликовано на сайте asleepysamurai.com 11 января 2019 г.