Одной из лучших особенностей 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-приложений.
Поделиться с друзьями
-->

Комментарии (16)


  1. theWaR_13
    23.05.2017 12:09

    Спасибо за статью, очень интересно. Такой вопрос возник. Вот вы написали, что вы группируете компоненты по области действия и у вас есть компонент CartTotal. Через какое-то время приложение выросло и этот же компонент нужно использовать в другом месте. Тут либо перетаскивать компонент в папку shared (папка с общими компонентами для всего приложения), но тогда придется также фиксить пути во всех файлах, где этот компонент используется. Либо же из того самого нового места, никак не связанного с Cart, импортировать CartTotal. И это тоже не очень правильно, ибо между не связанными частями появляется связь. Можно создать еще один такой же компонент, но это совсем неправильно, как мне кажется :)
    Не сталкивались ли вы с такой ситуацией? В своих проектах я всегда делю компоненты на dumb components и smart containers, такой проблемы не возникает.


    1. raveclassic
      23.05.2017 13:53

      Не сталкивались ли вы с такой ситуацией?
      Постоянно. Решение — не группировать в модули, иначе либо shared превратится в пышку, либо у вас появится пачка невнятных связующих модулей, либо просто модули будут ссылаться друг на друга.


    1. SPAHI4
      24.05.2017 12:09
      +1

      С перемещением компонента нет никаких проблем. IDE (Webstorm, например), сама меняет все импорты.


  1. DeLaVega
    23.05.2017 12:49

    PropType depricated в рамках реакта. Он отдельным модулем сейчас должен тянутся.


  1. seryh
    23.05.2017 12:53

    Не существует единственного правильного пути

    Это для меня в свое время оказалось большим недостатком. Когда нужно было быстро стартануть в проект, запутался в огромном количестве статей и советов, один лучше другого. Вся это react мешанина превратилась в конструктор "собери себе свой язык программирования", вот тебе ФП стиль, ООП, реактивный стиль, пот тебе строгая типизация (привет flow) и ворох либ к этому на любой вкус, написанных на 3 стандартах EcmaScript. В итоге быстрей удалось стартануть внезапно в ClojureScript'овском re-frame. Хотя до этого только бекенд на Clojure трогал.


    1. frol
      24.05.2017 09:03

      У меня была аналогичная ситуация, голова кругом шла от этих всех вариантов. В один прекрасный день появился Next.js и для меня это было идеальное коробочное решение без необходимости первый день проекта начинать с настройки вороха компонент (webpack, babel, react, router, ...) и громоздить тонны boilerplate'ов. Даже вот через полгода мой проект на Next.js в отличной форме, да ещё и Next.js улучшает мой проект с каждым релизом без моего активного вложения времени на изучение техник связанных с горячей перезагрузкой модулей или автоматическим разделением кода по роутам. Рекомендую взять Next.js на вооружение.


  1. x07
    23.05.2017 13:47
    -1

    Это далеко не лучшая особенность.


  1. Paul_Smith
    23.05.2017 13:48
    -1

    Статья отлично помогла освежить и структурировать в голове то, чем давно пользуюсь и убедиться, что я не один считаю это верным путем, спасибо.
    Только одно замечание. React — не фреймворк, это библиотека.И именно это и дает такую свободу организации кода.


  1. unsafePtr
    23.05.2017 16:21

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

    resolve: {
            alias: {
                "notification": path.resolve(rootDir, "web/notification/SmartNotification.min.js")
            }
    }
    


  1. Voronar
    24.05.2017 08:45

    Обычно, React-приложение — это SPA. Почему нет советов по организации роут-компонентов? Отдельно хранить их или все в одном файле?


    1. Voronar
      24.05.2017 08:50

      Или в одном файле, объединяющем несколько роутов по общей функциональной бизнес-сущности?


    1. VolCh
      24.05.2017 11:03

      Не во всех SPA есть роутинг, не во всех React-SPA с роутингом есть отдельные роут-компоненты, тем более в таком количестве, что нужна какая-то особая организация.


  1. VolCh
    24.05.2017 14:05

    Придерживаюсь подхода, при котором на первом уровне в src разделяются каталоги по функциональности, один из них (views) — для компонентов (и только для компонентов, ну может каких-то функций типа локализации и/или форматирования) Реакта. Заходишь во views и там всё про рендеринг из props/context компонента App.


  1. comerc
    25.05.2017 11:20

    Дополню свими изысканиями через боль по теме:


    Организация компонентов в проекте


    Ducks-pattern + redux-act


    Альтернативный заманчивый путь mobx-state-tree


  1. comerc
    25.05.2017 11:24

    Flow + tcomb заменяет prop-types, прибавляя возможностей.


  1. comerc
    25.05.2017 11:34

    И странно, что про lerna ни слова.