Одной из лучших особенностей React является то, что он не накладывает каких-либо ограничений на файловую структуру проекта. Поэтому на StackOverflow и аналогичных ресурсах так много вопросов о том, как структурировать React-приложения. Это очень спорная тема. Не существует единственного правильного пути. Предлагаем разобраться в этом вопросе с помощью статьи Джека Франклина, в которой он рассказывает о подходе к структурированию больших React-приложений. Здесь вы узнаете, какие решения можно принимать при создании React-приложений: о выборе инструментов, структурировании файлов и разбивки компонентов на более мелкие части.
Инструменты сборки и проверки кода
Webpack — отличный инструмент для сбора проектов. Несмотря на его сложность, тот факт, что команда отлично поработала над версией 2 и новым сайтом документации, значительно упрощает дело. Как только вы берете Webpack, имея ясную концепцию в голове, у вас действительно появляется невероятно мощный инструмент. Для компиляции кода можно воспользоваться Babel, в том числе для преобразований, специфичных для React: например, JSX и webpack-dev-server для локального «хостинга» сайта. Возможно, HMR не даст какую-то большую выгоду, поэтому достаточно будет использовать webpack-dev-server с его автоматическим обновлением страницы.
Также для импорта и экспорта зависимостей будем использовать синтаксис модулей ES2015 (который транспилируется Babel). Этот синтаксис существует уже давно, и, хотя Webpack поддерживает CommonJS (синтаксис импорта в стиле Node), лучше использовать самое последнее и лучшее. К тому же, Webpack может удалять мертвый код из бандла, используя модули ES2015, что, хотя и не идеально, но является очень удобной функцией, которая станет более полезной, когда сообщество перейдет к публикации кода в npm в стандарте ES2015.
Конфигурирование разрешения модулей Webpack
Единственное, что может разочаровывать при работе с большими проектами с вложенной файловой структурой, — это определение относительных путей между файлами. Вы обнаружите, что у вас много кода, который выглядит примерно так:
import foo from './foo'
import bar from '../../../bar'
import baz from '../../lib/baz'
Когда вы создаете свое приложение с помощью Webpack, то можете указать каталог, в котором Webpack должен искать файл, если он сам не может его найти. Это позволяет определить базовую папку, к которой относится весь импорт. Например, можно всегда помещать свой код в каталог src. И можно заставить Webpack всегда искать в этом каталоге. Это делается там же, где вы информируете Webpack о любых других расширениях файлов, которые вы, возможно, используете, например jsx:
// inside Webpack config object
{
resolve: {
modules: ['node_modules', 'src'],
extensions: ['.js', '.jsx'],
}
}
Значением по умолчанию для resolve.modules является ['node_modules']
, поэтому его тоже нужно добавить, иначе Webpack не сможет импортировать файлы, установленные с помощью npm или yarn.
После этого вы всегда можете импортировать файлы относительно каталога src:
import foo from './foo'
import bar from 'app/bar' // => src/app/bar
import baz from 'an/example/import' // => src/an/example/import
Хотя это и завязывает ваш код приложения на Webpack, пожалуй, это выгодный компромисс, потому что он облегчает вам выполнение кода и позволяет гораздо проще добавлять импорты.
Структура каталогов
Нет единственно правильной структуры каталогов для всех приложений React. Как и для остальных частей этой статьи, вам следует изменить структуру в соответствии со своими предпочтениями. Ниже описывается один из примеров хорошо работающей структуры.
Код живет в src
Чтобы все было организовано, помещаем весь код приложения в каталог под названием src. Он содержит только код, который сводится в окончательный бандл, и больше ничего. Это полезно, потому что вы можете указать Babel (или любому другому инструменту, обрабатывающему код) просто посмотреть в одном каталоге и убедиться, что он не обрабатывает какой-либо код, в котором он не нуждается. Другой код, такой как файлы конфигурации Webpack, находится в соответствующем каталоге. Например, структура каталогов верхнего уровня может содержать:
- src => app code here
- webpack => webpack configs
- scripts => any build scripts
- tests => any test specific code (API mocks, etc)
Как правило, единственными файлами на верхнем уровне являются index.html, package.json и любые dotfiles, такие как .babelrc. Некоторые предпочитают включать конфигурацию Babel в package.json, но в крупных проектах со многими зависимостями эти файлы могут стать слишком большими, поэтому целесообразно использовать .eslintrc, .babelrc и т.д.
Сохраняя код приложения в src, вы также можете использовать настройку resolve.modules
, о которой упоминалось выше, что упрощает импорт.
React-компоненты
Определившись с каталогом src, нужно решить, как структурировать компоненты. Если их все помещать в одну большую папку, такую как src/components, то в больших проектах она очень быстро захламляется.
Общей тенденцией является наличие отдельных папок для «умных» и «глупых» компонентов (также известных как контейнерные и презентационные компоненты), но такое явное деление не всегда полезно. И хотя у вас наверняка есть компоненты, которые можно классифицировать как «умные» и «глупые» (об этом ниже), не обязательно создавать папки для каждой из этих категорий.
Мы сгруппировали компоненты на основе областей приложения, в которых они используются, наряду с каталогом core для общих компонентов, которые используются повсюду (кнопки, верхние и нижние колонтитулы — компоненты, которые являются универсальными и многоразовыми). Остальные каталоги соответствуют определенным областям приложения. Например, у нас есть каталог с именем cart, который содержит все компоненты, связанные с корзиной покупок, и каталог под названием listings, который содержит код для списков вещей, которые пользователи могут купить на странице.
Группировка по каталогам также означает, что можно избежать лишних префиксов, указывающих на область приложения, в которой используются компоненты. Например, если у нас есть компонент, который отображает общую стоимость корзины пользователя, можно назвать его Total, а не CartTotal, потому что он импортируется из каталога cart:
import Total from 'src/cart/total'
// vs
import CartTotal from 'src/cart/cart-total'
Это правило можно иногда нарушать: порой дополнительный префикс может внести дополнительную ясность, особенно если у вас есть 2-3 аналогично названных компонента. Но зачастую этот метод позволяет избежать повторения имен.
Расширение jsx вместо заглавных букв
Многие используют в названиях файлов с React-компонентами заглавные буквы, чтобы отличать их от обычных файлов JavaScript. Таким образом, в вышеупомянутом импорте файлы будут называться CartTotal.js, или Total.js. Но можно придерживаться строчных букв с дефисами в качестве разделителей, то есть для различения React-компонентов использовать расширение файлов .jsx: cart-total.jsx.
Это дает небольшое дополнительное преимущество: можно легко искать только ваши файлы React, ограничивая поиск в файлах по .jsx, и вы даже можете при необходимости применять к ним специфические плагины Webpack.
Какое бы соглашение об именовании файлов вы не выбрали, важно придерживаться его. Наличие комбинации нескольких условных обозначений в вашем приложении быстро станет кошмаром, в котором вам придется как-то ориентироваться.
Только один компонент в файле
Следуя предыдущему правилу, мы придерживаемся соглашения, по которому в одном файле у нас всегда должен быть только один компонент, и компонент всегда должен быть экспортом по умолчанию.
Обычно наши файлы React выглядят так:
import React, { Component, PropTypes } from 'react'
export default class Total extends Component {
...
}
В случае, когда мы должны обернуть компонент, чтобы подключить его, например, к хранилищу данных Redux, полностью обернутый компонент становится экспортом по умолчанию:
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
export class Total extends Component {
...
}
export default connect(() => {...})(Total)
Вы заметили, что мы по-прежнему экспортируем оригинальный компонент? Это действительно полезно для тестирования, когда вы можете работать с «простым» компонентом, а не настраивать Redux в своих модульных тестах.
Экспортируя компонент по умолчанию, легко импортировать компонент и знать, как его получить, вместо того, чтобы искать точное имя. Один из недостатков этого подхода заключается в том, что импортирующий пользователь может вызывать компонент как угодно. Отметим еще раз, у нас есть соглашение для этого: импорт должен производиться по имени файла. Поэтому, если вы импортируете total.jsx, то компонент должен быть назван Total. user-header.jsx становится UserHeader, и так далее.
«Умные» и «глупые» React-компоненты
Выше кратко упоминалось о разделении компонентов на «умные» и «глупые». И хотя мы не раскладываем их по отдельным каталогам, вы можете в общих чертах разделить приложение на эти два типа компонентов:
- «Умные» компоненты манипулируют данными, подключаются к Redux и имеют дело с пользовательским взаимодействием.
- «Глупые» компоненты лишь предоставляют набор свойств для отображения некоторых данных на экране.
«Глупые» компоненты составляют основную часть нашего приложения, и, если это возможно, вы всегда должны отдавать им предпочтение. С ними легче работать, меньше проблем, и их легче тестировать.
Даже когда нам приходится создавать «умные» компоненты, мы пытаемся сохранить всю логику JavaScript в отдельном файле. В идеальном случае компоненты, манипулирующие данными, должны передавать эти данные некоторому JavaScript, который фактически и будет это делать. Тогда код манипулирования можно протестировать отдельно от React, и вы можете делать с ним что угодно при тестировании React-компонента.
Избегайте больших методов render
Одна вещь, к которой мы стремимся, — иметь много маленьких React-компонентов, а не меньшее количество более крупных. Хорошим индикатором, что ваш компонент становится слишком большим, является размер функции рендеринга. Если она становится громоздкой, или вам необходимо разбить её на несколько меньших функций, то, возможно, пришло время подумать о разделении компонента.
Это не жесткое правило; вы и ваша команда должны чётко понимать, что для вас считается «большим» компонентом, прежде чем увеличивать их количество. Но размер функции render
компонента является хорошим ориентиром. Вы также можете использовать количество props или items в состоянии как еще один хороший индикатор. Если компонент принимает семь различных props, это может быть признаком того, что он делает слишком много.
Всегда используйте prop-type
React позволяет, используя пакет prop-types, документировать имена и типы свойств, которые, как вы ожидаете, будут переданы компоненту. Обратите внимание, что в React 15.5 это не так, ранее proptypes был частью модуля React.
Объявляя имена и типы ожидаемых свойств, а также то, являются ли они опциональными, вы должны чувствовать себя уверенно в работе с компонентами, и тратить меньше времени на отладку, если забыли имя свойства или присвоили ему неправильный тип. Этого можно добиться с помощью ESLint-React PropTypes rule.
Может показаться, что время на их добавление будет потрачено зря. Но если вы это сделаете, то будете благодарить себя, собравшись повторно использовать компонент, который написали полгода назад.
Redux
Мы используем Redux для управления данными во многих наших приложениях, а структурирование приложений Redux — это еще один очень распространенный вопрос, по которому есть множество разных мнений.
Победителем для нас является Ducks, который помещает в один файл экшены (actions), reducer и action creators для каждой части вашего приложения.
Вместо того, чтобы иметь reducers.js и actions.js, каждый из которых содержит куски кода для связи друг с другом, система Ducks утверждает, что имеет смысл группировать связанный код в один файл. Предположим, у вас есть Redux store с двумя ключами верхнего уровня, user
и posts
. Ваша структура папок будет выглядеть так:
ducks
- index.js
- user.js
- posts.js
index.js будет содержать код, который создает основной reducer, возможно, используя combineReducers
из Redux, а в user.js и posts.js вы поместите весь код для них, который обычно выглядит так:
// user.js
const LOG_IN = 'LOG_IN'
export const logIn = name => ({ type: LOG_IN, name })
export default function reducer(state = {}, action) {
..
}
Это избавляет от необходимости импортировать actions и action creators из разных файлов и позволяет хранить рядом код для разных частей вашего хранилища.
Автономные модули JavaScript
Хотя в этой статье основное внимание уделялось React-компонентам, но при создании React-приложения вы можете написать много кода, полностью отделенного от React.
Каждый раз, когда вы находите компонент с бизнес-логикой, которая может быть удалена из компонента, рекомендуется это сделать. Обычно хорошо работает каталог с именем lib или services — конкретное имя не имеет значения, но каталог, полный «не-React компонентов», действительно то, что вам нужно.
Эти службы иногда экспортируют группу функций, или объект связанных функций. Например, у нас есть services/local-storage
, который предоставляет небольшую оболочку вокруг нативного API-интерфейса window.localStorage
:
// services/local-storage.js
const LocalStorage = {
get() {},
set() {},
...
}
export default LocalStorage
Хранение вашей логики отдельно от таких компонентов имеет некоторые, действительно большие, преимущества:
- Вы можете протестировать этот код изолированно, без необходимости рендеринга каких-либо React-компонентов.
- В ваших React-компонентах вы можете заглушить службы, чтобы иметь данные, которые нужны для конкретного теста.
Тесты
Фреймворк Jest Facebook — отличный инструмент для тестирования. Он очень быстро и хорошо справляется с множеством тестов, быстро запускается в режиме просмотра, оперативно дает вам обратную связь и из коробки предоставляет некоторые удобные функции для тестирования React. Рассмотрим, как можно структурировать тесты.
Кто-то скажет, что лучше иметь отдельный каталог, который содержит все тесты для всех задач. Если у вас есть src/app/foo.jsx, то будет также tests/app/foo.test.jsx. Но на практике, когда приложение разрастается, это затрудняет поиск нужных файлов. И если вы перемещаете файлы в src, то часто забываете перемещать их в test, и структуры теряют синхронность. Кроме того, если у вас есть файл в tests, в котором необходимо импортировать файл из src, вы получите очень длинный импорт. Наверняка все сталкивались:
import Foo from '../../../src/app/foo'
С этим трудно работать, и это трудно исправлять, если вы меняете структуру каталогов.
Зато размещение каждого файла тестов вместе с файлом исходников позволяет избежать всех этих проблем. Чтобы отличить их, мы добавляем в наши тесты суффикс .spec, хотя другие используют .test или просто -test, но все они живут рядом с файлами с исходным кодом с тем же именем:
- cart
— total.jsx
— total.spec.jsx
- services
— local-storage.js
— local-storage.spec.js
По мере изменения структуры каталогов легко перемещать правильные тестовые файлы. Также сразу заметно, когда файл не имеет никаких тестов. Можно быстро выявить эти проблемы и исправить их.
Выводы
Есть много способов добиться своего, это можно сказать и о React. Одна из лучших особенностей фреймворка — это то, как он позволяет вам принимать большинство решений относительно инструментария, средств сборки и структуры каталогов. Надеемся, что эта статья дала вам несколько идей о том, как можно подойти к структурированию ваших более крупных React-приложений.
Автор: NIX_Solutions