Статьи

Как организовать большое приложение React и сделать его масштабируемым

Эта статья написана гостем Джеком Франклином . Гостевые посты SitePoint нацелены на то, чтобы привлечь к вам интересный контент от известных авторов и докладчиков веб-сообщества.

В этой статье я расскажу о подходе, который я использую при создании и структурировании больших приложений React. Одна из лучших особенностей React — это то, как он уходит с вашего пути и не является описательным, когда речь идет о файловой структуре. Таким образом, вы найдете много вопросов о переполнении стека и подобных сайтах, спрашивающих, как структурировать приложения. Это очень самоуверенная тема, и нет единственно верного пути. В этой статье я расскажу вам о решениях, которые я принимаю при создании приложений React: подбирая инструменты, структурируя файлы и разбивая компоненты на более мелкие части.

Если вам понравился этот пост, вы также можете зарегистрироваться в SitePoint Premium и посмотреть наш курс по работе с формами с использованием React и Redux .

Космонавт строит космическую колонию в форме логотипа React

Инструменты для сборки и Linting

Некоторые из вас не удивятся, что я большой поклонник Webpack для создания своих проектов. Несмотря на то, что это сложный инструмент, отличная работа, проделанная командой над версией 2 и новым сайтом документации, делает его намного проще. Как только вы попадаете в Webpack и у вас в голове есть идеи, вы действительно получаете невероятную силу для использования. Я использую Babel для компиляции своего кода, включая специфичные для React преобразования, такие как JSX, и webpack-dev-server для локального обслуживания моего сайта. Лично я не обнаружил, что горячая перезагрузка дает мне такое преимущество, поэтому я более чем доволен работой webpack-dev-server и его автоматическим обновлением страницы.

Я также использую синтаксис модуля ES2015 (который передается через Babel) для импорта и экспорта зависимостей. Этот синтаксис существует уже некоторое время, и хотя Webpack может поддерживать CommonJS (иначе как импорт в стиле Node), для меня имеет смысл начать использовать последнюю версию и лучшую. Кроме того, Webpack может удалять мертвый код из комплектов с помощью модулей ES2015, что, хотя и не идеально, является очень удобной функцией, которая станет более полезной, когда сообщество перейдет к публикации кода на npm в ES2015.

Настройте разрешение modules 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 и любые точечные файлы, такие как .babelrc . Некоторые предпочитают включать конфигурацию Babel в package.json , но я считаю, что эти файлы могут .eslintrc в больших проектах с большим количеством зависимостей, поэтому мне нравится использовать .eslintrc , .babelrc и так далее.

Сохраняя код приложения в src , вы также можете использовать трюк resolve.modules я упоминал ранее, что упрощает весь импорт.

Реагировать Компоненты

Когда у вас есть папка src , вам нужно решить, как структурировать ваши компоненты. В прошлом я помещал все компоненты в одну большую папку, например, src/components , но я обнаружил, что в больших проектах это очень быстро перегружается.

Распространенной тенденцией является наличие папок для «умных» и «немых» компонентов ( также известных как «контейнерные» и «презентационные» компоненты ), но лично я никогда не находил, что папки работают для меня. Несмотря на то, что у меня есть компоненты, которые можно условно разделить на «умные» и «глупые» (я расскажу об этом ниже), у меня нет конкретных папок для каждой из них.

Мы сгруппировали компоненты на основе областей приложения, которые они используют, наряду с core папкой для общих компонентов, которые используются повсеместно (кнопки, верхние и нижние колонтитулы — компоненты, которые являются общими и могут многократно использоваться). Остальные папки отображаются в определенной области приложения. Например, у нас есть папка cart которая содержит все компоненты, относящиеся к представлению корзины покупок, и папка, которая содержит код для перечисления товаров, которые пользователи могут купить на странице.

Распределение по категориям также означает, что вы можете избежать добавления компонентов к области приложения, для которой они используются. Например, если бы у нас был компонент, который отображает общую стоимость корзины пользователя, а не называет его CartTotal я мог бы предпочесть использовать Total , потому что я импортирую его из папки cart :

 import Total from 'src/cart/total' // vs import CartTotal from 'src/cart/cart-total' 

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

Предпочитаю расширение jsx

Многие люди называют компоненты React заглавной буквой в файле, чтобы отличать их от обычных файлов JavaScript. Таким образом, в приведенном выше импорте файлы будут CartTotal.js или Total.js . Я предпочитаю придерживаться строчных файлов с тире в качестве разделителей, поэтому, чтобы отличить их, я использую расширение .jsx для компонентов React. Поэтому я бы придерживался cart-total.jsx .

Это дает небольшое дополнительное преимущество, .jsx возможности простого поиска по вашим файлам React, ограничивая поиск файлами с .jsx , и вы даже можете применять к этим файлам определенные плагины Webpack, если вам это нужно.

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

Один компонент реагирования на файл

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

Обычно наши файлы 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 и так далее.

«Умные» и «тупые» реактивные компоненты

Я кратко упомянул разделение «умных» и «немых» компонентов, и это то, чего мы придерживаемся в нашей базе кода. Хотя мы не распознаем его, разбив их на папки, вы можете широко разделить наше приложение на два типа компонентов:

  • «Умные» компоненты, которые управляют данными, подключаются к Redux и взаимодействуют с пользователем
  • «Тупые» компоненты, которые получают набор реквизита и выводят некоторые данные на экран.

Подробнее о том, как мы нацелены на «тупые» компоненты, вы можете прочитать в моем блоге «Функциональные компоненты без состояния в React» . Эти компоненты составляют большую часть нашего приложения, и вы всегда должны отдавать предпочтение этим компонентам, если это возможно. С ними легче работать, меньше глючить и легче тестировать.

Даже когда нам приходится создавать «умные» компоненты, мы стараемся хранить всю логику JavaScript в своем собственном файле. В идеале компоненты, которые должны манипулировать данными, должны передавать эти данные некоторому JavaScript, который может манипулировать ими. Таким образом, код манипуляции можно тестировать отдельно от React, и вы можете при необходимости смоделировать его при тестировании компонента React.

Избегайте больших методов render

Одна вещь, к которой мы стремимся, — это иметь много маленьких компонентов React, а не меньше, более крупных компонентов. Хорошим руководством для того, когда ваш компонент становится слишком большим, является размер функции рендеринга. Если это становится громоздким, или вам нужно разделить его на множество более мелких функций рендеринга, возможно, настало время подумать об абстрагировании функции.

Это не жесткое правило; Вы и ваша команда должны получить представление о размере компонента, которым вы довольны, прежде чем извлекать больше компонентов, но размер функции render компонента является хорошей мерой измерения. Вы также можете использовать количество реквизита или предметов в состоянии в качестве другого хорошего индикатора. Если компонент принимает семь разных реквизитов, это может быть признаком того, что он делает слишком много.

Всегда используйте prop-type

React позволяет вам задокументировать имена и типы свойств, которые вы ожидаете от компонента, используя его пакет prop-types . Обратите внимание, что это изменилось с React 15.5 . Ранее, прототипы были частью модуля React.

Объявляя имена и типы ожидаемых реквизитов, а также то, являются ли они необязательными или нет, вы получаете больше уверенности при работе с компонентами, что у вас есть нужные свойства, и тратите меньше времени на отладку, если вы забыли имя свойства или дали ему неправильный тип. Вы можете применить это с помощью правила ESLint-React PropTypes .

Хотя потратить время на их добавление может оказаться бесполезным, когда вы это сделаете, вы поблагодарите себя за повторное использование компонента, который вы написали шесть месяцев назад.

Redux

Мы также используем Redux во многих наших приложениях для управления данными в нашем приложении, и структурирование приложений Redux — это еще один очень распространенный вопрос со многими различными мнениями.

Для нас победителем стало Ducks , предложение, которое помещает ваши действия, создателей и создателей действий для каждой части вашего приложения в один файл.

Вместо того, чтобы иметь reducers.js и actions.js , где каждый содержит биты кода, связанные друг с другом, система Ducks утверждает, что имеет больше смысла сгруппировать связанный код вместе в один файл. Допустим, у вас есть магазин Redux с двумя ключами верхнего уровня, user и posts . Структура вашей папки будет выглядеть так:

 ducks - index.js - user.js - posts.js 

index.js будет содержать код, который создает основной редуктор, возможно, для этого используется 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) { … } 

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

Автономные модули 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 из коробки. Ранее я много писал об этом в SitePoint , поэтому здесь я не буду вдаваться в подробности, но я расскажу о том, как мы структурируем наши тесты.

В прошлом я стремился иметь отдельную папку для tests которой содержались все тесты для всего. Так что если бы у вас был src/app/foo.jsx , у вас тоже был бы tests/app/foo.test.jsx . На практике, когда приложение становится больше, это затрудняет поиск нужных файлов, и если вы перемещаете файлы в src , вы часто забываете перемещать их в test , и структуры не синхронизируются. Кроме того, если у вас есть файл в tests который должен импортировать файл в src , вы получите очень длинный импорт. Я уверен, что мы все сталкивались с этим:

 import Foo from '../../../src/app/foo' 

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

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

 - cart - total.jsx - total.spec.jsx - services - local-storage.js - local-storage.spec.js 

По мере изменения структуры папок легко перемещать нужные тестовые файлы, и это также невероятно очевидно, когда файл не имеет каких-либо тестов, поэтому вы можете обнаружить эти проблемы и исправить их.

Вывод

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