Большинство руководств по React предлагают группировать файлы по назначению, например вы должны поместить все свои действия в единую папку под названием «actions», в то время как ваши редукторы должны храниться в папке «redurs». Например, вначале это могло выглядеть так:

actions/
  users.js
  index.js
components/
  UserList.js
  index.js
reducers/
  users.js
  index.js

Этот подход отлично работает вначале, но когда ваше приложение начинает расти, становится труднее ориентироваться в кодовой базе, а также вызывает более сильную связь между вашими модулями.

Например, если нашему UserList компоненту требуется доступ к пользователям, которые вызываются вызовом API (который вызывается действием), ему нужно будет импортировать действие из папки actions. Наш редуктор также должен импортировать действие, чтобы правильно с этим справиться.

Как только мы поняли, что прямой доступ к хранилищу из компонента увеличивает нашу связь, мы решили переключиться на концепцию геттеров, поэтому нам пришлось ввести новую корневую папку с именем getters.

Когда наши функции получения данных стали достаточно сложными, мы решили переключиться на саги, и было очевидно, что саги должны храниться в отдельной папке с именем sagas.

Давайте посмотрим на полученную структуру каталогов и зависимости между файлами:

actions/
  users.js
  index.js
components/
  UserList.js (imports getters/users, actions/users)
  index.js
reducers/
  users.js (imports actions/users)
  index.js
sagas/
  users.js (imports getters/users, actions/users)
  index.js
getters/
  users.js
  index.js

Давайте подытожим все проблемы, которые у нас есть с этой структурой:

  1. Папка components становится больше, и трудно понять, какие компоненты можно повторно использовать - без этой видимости вы можете легко разбить вещи, изменив компонент, который (неожиданно!) Используется где-то еще в вашем приложении.
  2. высокая степень связи между модулями и низкая связность внутри модуля (представьте, что мы представляем компонент CustomerList и все файлы, необходимые для его работы: редуктор клиентов не заботится о пользователях редуктор вообще)
  3. приложение становится очень трудно модульным - вы не можете легко отделить часть вашего кода и заменить ее чем-то новым

На этом этапе мы решили перейти на более доменно-ориентированную архитектуру, которая описана в статье Франсуа Занинотто Лучшая файловая структура для приложений React / Redux.

Основная идея статьи заключается в том, что файлы сгруппированы не по природе, а по бизнес-домену, а вся конфигурация и другие общие вещи хранятся в специальной папке под названием app . Например, если следовать описанному в статье способу, структура нашего проекта может быть трансформирована примерно так:

app/
  Avatar.js
  routes.js
  app.js
customers/
  CustomerList.js
  customers.actions.js
  customers.api.js
  customers.getters.js
  customers.reducers.js
  customers.sagas.js
  index.js
users/
  UserList.js
  users.actions.js
  users.api.js
  users.getters.js
  users.reducers.js
  users.sagas.js
  index.js

Обратите внимание, что тесты стоит перенести из выделенного каталога (например, __tests__ или чего-то еще) в домены. Мы предпочитаем давать тестовому файлу то же имя, что и тестируемому файлу, но с добавлением -test в конце, например если нам нужно протестировать users.reducers.js, тестовый файл будет называться users.reducers-test.js.

Это уже здорово, но мы внесли некоторые коррективы. Прежде всего, наши компоненты - это не просто файлы, а папки, которые содержат index.js, стили, HOC, тесты и т. Д. Затем, чтобы сделать зависимости наших модулей более понятными и избежать циклов зависимостей, мы решили добавить некоторые более строгие правила для импорт и экспорт:

  1. Все общие компоненты помещаются в папку app/components. В качестве альтернативы вы можете ввести внешнюю библиотеку компонентов и сохранить ее в отдельном репо.
  2. Когда вы импортируете что-то из папки app, вы должны импортировать, используя полный относительный путь, например если вы хотите использовать Avatar в UserList, ваш импорт должен быть import Avatar from '../../app/components/Avatar' (не import { Avatar } from '../../app/components' или даже import { Avatar } from '../../app')
  3. Когда вы импортируете что-то извне домена, вы всегда должны импортировать из корня, например если вам нужен getCustomer геттер в вашем UserList компоненте, вы должны импортировать его следующим образом: import { getCustomer } from '../../app/customers'
  4. Когда вы импортируете что-то изнутри домена, вы всегда должны импортировать, используя полный относительный путь, например если вам нужен getCustomer геттер в CustomerList компоненте, вы должны импортировать его следующим образом: import { getCustomer } from '../customers.getters'
  5. Все, что предполагается использовать за пределами папки домена (корневые компоненты, геттеры, редукторы, саги и т. Д.), Должно быть явно экспортировано в index.js
  6. Если вы понимаете, что у вас есть поддомен, вы просто создаете папку внутри родительского домена, заполняйте ее в соответствии с приведенными выше правилами. Не забудьте экспортировать все, что вам нужно, через родительский домен, другие домены не должны знать о внутреннем устройстве.

Нет общего правила о том, как лучше всего разделить ваше приложение на домены. В случае с OGE мы работаем над внутренним приложением для сотрудников компании, поэтому прямо сейчас наши домены в основном сопоставляются с нашими основными сущностями, такими как клиенты, пользователи и т. Д. Если вы работаете над чем-то вроде социальной сети или электронной почты. коммерческий сайт, ваши домены могут немного отличаться. Например, у вас может быть огромный отдельный домен, связанный с процессом регистрации и адаптации ваших новых пользователей.

В качестве расширения пункта 4 мы решили, что каждый модуль должен экспортировать только одну корневую сагу и один корневой редуктор и следовать этому соглашению в файлах конфигурации. Например, index.js для домена пользователей может выглядеть так:

import { all, spawn } from 'redux-saga/effects'
import {
  users,
  isLoadingUsers
} from './users.reducers'
import {
  watchRequestUsers,
  watchRequestUser
} from './users.sagas'
export UserList from './UserList'
export {
  fetchUsers,
  fetchUser
} from './users.actions'
export {
  getUsers,
  getUser
} from './users.getters'
export function* usersSagas() {
  yield all([
    spawn(watchRequestUsers),
    spawn(watchRequestUser)
  ])
}
export const usersReducers = {
  users,
  isLoadingUsers
}

В результате мы получили следующую структуру:

app/
  components/
    Avatar/
      index.js
      Avatar.scss
  routes.js
  app.js
customers/
  CustomerList/
    index.js
    CustomerList.scss
  customers.actions.js
  customers.api.js
  customers.getters.js
  customers.getters-test.js
  customers.reducers.js
  customers.reducers-test.js
  customers.sagas.js
  customers.sagas-test.js
  index.js
users/
  UserList/
    index.js
    UserList.scss
  users.actions.js
  users.api.js
  users.getters.js
  users.getters-test.js
  users.reducers.js
  users.reducers-test.js
  users.sagas.js
  users.sagas-test.js
  index.js

Мы с нетерпением ждем возможности узнать о том, как вы организуете свои проекты React разного размера, и о проблемах, с которыми вы сталкиваетесь.