Большинство руководств по 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
Давайте подытожим все проблемы, которые у нас есть с этой структурой:
- Папка
components
становится больше, и трудно понять, какие компоненты можно повторно использовать - без этой видимости вы можете легко разбить вещи, изменив компонент, который (неожиданно!) Используется где-то еще в вашем приложении. - высокая степень связи между модулями и низкая связность внутри модуля (представьте, что мы представляем компонент CustomerList и все файлы, необходимые для его работы: редуктор клиентов не заботится о пользователях редуктор вообще)
- приложение становится очень трудно модульным - вы не можете легко отделить часть вашего кода и заменить ее чем-то новым
На этом этапе мы решили перейти на более доменно-ориентированную архитектуру, которая описана в статье Франсуа Занинотто Лучшая файловая структура для приложений 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, тесты и т. Д. Затем, чтобы сделать зависимости наших модулей более понятными и избежать циклов зависимостей, мы решили добавить некоторые более строгие правила для импорт и экспорт:
- Все общие компоненты помещаются в папку
app/components
. В качестве альтернативы вы можете ввести внешнюю библиотеку компонентов и сохранить ее в отдельном репо. - Когда вы импортируете что-то из папки
app
, вы должны импортировать, используя полный относительный путь, например если вы хотите использоватьAvatar
вUserList
, ваш импорт должен бытьimport Avatar from '../../app/components/Avatar'
(неimport { Avatar } from '../../app/components'
или дажеimport { Avatar } from '../../app')
- Когда вы импортируете что-то извне домена, вы всегда должны импортировать из корня, например если вам нужен
getCustomer
геттер в вашемUserList
компоненте, вы должны импортировать его следующим образом:import { getCustomer } from '../../app/customers'
- Когда вы импортируете что-то изнутри домена, вы всегда должны импортировать, используя полный относительный путь, например если вам нужен
getCustomer
геттер вCustomerList
компоненте, вы должны импортировать его следующим образом:import { getCustomer } from '../customers.getters'
- Все, что предполагается использовать за пределами папки домена (корневые компоненты, геттеры, редукторы, саги и т. Д.), Должно быть явно экспортировано в
index.js
- Если вы понимаете, что у вас есть поддомен, вы просто создаете папку внутри родительского домена, заполняйте ее в соответствии с приведенными выше правилами. Не забудьте экспортировать все, что вам нужно, через родительский домен, другие домены не должны знать о внутреннем устройстве.
Нет общего правила о том, как лучше всего разделить ваше приложение на домены. В случае с 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 разного размера, и о проблемах, с которыми вы сталкиваетесь.