По мере роста объема, размера и набора функций вашего приложения 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 г.