Статьи

5 React Архитектура Лучшие практики

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

Однако, поскольку React заботится только о уровне представления приложения, он не применяет какую-либо конкретную архитектуру (например, MVC или MVVM). Это может затруднить организацию вашей кодовой базы по мере роста вашего проекта React.

Здесь, на 9elements (где я являюсь генеральным директором), одним из наших ведущих продуктов является PhotoEditorSDK — полностью настраиваемый редактор фотографий, который легко интегрируется в ваше приложение HTML5, iOS или Android. PhotoEditorSDK — это масштабное приложение React, предназначенное для разработчиков. Он требует высокой производительности, небольших сборок и должен быть очень гибким в отношении стиля и особенно тематики.

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

1. Расположение каталога

Первоначально стиль и код для наших компонентов были разделены. Все стили находятся в общем CSS-файле (мы используем SCSS для предварительной обработки). Фактический компонент (в данном случае FilterSlider ) был отделен от стилей:

 ├── components │ └── FilterSlider │ ├── __tests__ │ │ └── FilterSlider-test.js │ └── FilterSlider.jsx └── styles └── photo-editor-sdk.scss 

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

 components └── FilterSlider ├── __tests__ │ └── FilterSlider-test.js ├── FilterSlider.jsx └── FilterSlider.scss 

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

Импорт компонентов

Одним из недостатков этой структуры каталогов является то, что для импорта компонентов требуется импортировать полный путь, например:

 import FilterSlider from 'components/FilterSlider/FilterSlider' 

Но то, что мы действительно хотели бы написать, это:

 import FilterSlider from 'components/FilterSlider' 

Наивный подход к решению этой проблемы — изменить файл компонента на index.js :

 components └── FilterSlider ├── __tests__ │ └── FilterSlider-test.js ├── FilterSlider.scss └── index.jsx 

К сожалению, при отладке компонентов React в Chrome и возникновении ошибки отладчик покажет вам много файлов index.js , и это сделало эту опцию index.js .

Другой подход, который мы попробовали, был каталог-named-webpack-plugin . Этот плагин создает небольшое правило в распознавателе веб-пакетов для поиска файла JavaScript или JSX с тем же именем, что и каталог, из которого он импортируется. Недостатком этого подхода является привязка поставщика к веб-пакету. Это серьезно, потому что Rollup немного лучше для объединения библиотек. Кроме того, обновление до последних версий веб-пакетов всегда было проблемой.

Решение, которое мы получили, немного более обширно, но использует стандартный механизм разрешения Node.js, что делает его надежным и перспективным. Все, что мы делаем, это добавляем файл package.json в структуру файла:

 components └── FilterSlider ├── __tests__ │ └── FilterSlider-test.js ├── FilterSlider.jsx ├── FilterSlider.scss └── package.json 

А в package.json мы используем свойство main, чтобы установить нашу точку входа для компонента, вот так:

 { "main": "FilterSlider.jsx" } 

С этим дополнением мы можем импортировать такой компонент:

 import FilterSlider from 'components/FilterSlider' 

2. CSS в JavaScript

Стиль, и особенно тематика, всегда были проблемой. Как упоминалось выше, в нашей первой итерации приложения у нас был большой файл CSS (SCSS), в котором жили все наши классы. Чтобы избежать конфликтов имен, мы использовали глобальный префикс и следовали соглашениям БЭМ для создания имен правил CSS. Когда наше приложение росло, этот подход не очень хорошо масштабировался, поэтому мы искали замену. Сначала мы оценили модули CSS , но в то время у них были некоторые проблемы с производительностью. Кроме того, извлечение CSS через плагин Extract Text из веб-пакета не сработало так хорошо (на момент написания статьи все должно быть в порядке). Кроме того, этот подход создал сильную зависимость от веб-пакета и сделал тестирование довольно сложным.

Затем мы оценили некоторые другие решения CSS-in-JS, которые недавно появились на сцене:

  • Styled Components : самый популярный выбор с самым большим сообществом
  • EmotionJS : горячий конкурент
  • Glamorous : еще одно популярное решение CSS-in-JS.

Выбор одной из этих библиотек сильно зависит от вашего варианта использования:

  1. Вам нужна библиотека, чтобы выкладывать скомпилированный файл CSS для производства? EmotionJS может сделать это!
  2. У вас есть сложные темы? Стильные компоненты и Glamorous — ваши друзья!
  3. Нужно ли запускать на сервере? Это не проблема для последних версий всех библиотек!

Для PhotoEditorSDK мы фактически создали Adonis — наше собственное решение CSS-in-JS. Не стесняйтесь проверить проект и оценить, соответствует ли он вашим потребностям, но, честно говоря, мы не можем рекомендовать такой подход всем. Для большинства других проектов мы обычно используем Styled Components , так как он действительно мощный и имеет сильное сообщество. Это действительно полезно, особенно если у вас действительно сложные тематические проблемы. Плагины сообщества, такие как стиль оформления, являются бесценным ресурсом.

Подсказка : при стилизации большого количества тегов HTML, таких как:

 const Wrapper = styled.section` padding: 4em; background: papayawhip; `; 

мы создаем файл Atoms.jsx который помещаем все стилизованные компоненты:

 components └── FilterSlider ├── __tests__ │ └── FilterSlider-test.js ├── Atoms.jsx ├── FilterSlider.jsx ├── FilterSlider.scss └── package.json 

Хорошей практикой является удаление вашего основного файла компонента.

Стремление к единой ответственности компонентов React

Когда вы разрабатываете абстрактные компоненты пользовательского интерфейса, иногда трудно разделить проблемы. В какой-то момент ваш компонент будет нуждаться в определенной доменной логике из вашей модели, и тогда все станет запутанным. В следующих разделах мы хотели бы показать вам определенные методы для СУШКИ ваших компонентов. Следующие методы частично совпадают по функциональности, и выбор правильного варианта для вашей архитектуры — это скорее предпочтение стиля, а не основанный на неопровержимых фактах. Но позвольте мне сначала представить варианты использования:

  • Нам пришлось ввести механизм для работы с компонентами, которые учитывают контекст вошедшего в систему пользователя.
  • Нам пришлось визуализировать таблицу с несколькими складными элементами <tbody> .
  • Нам пришлось отображать разные компоненты в зависимости от разных состояний.

В следующем разделе я покажу различные решения проблем, описанных выше.

3. Компоненты высшего порядка (HOCs)

Иногда вы должны убедиться, что компонент React отображается только тогда, когда пользователь вошел в ваше приложение. Первоначально вы будете выполнять некоторые проверки работоспособности в вашем методе render пока не обнаружите, что много повторяете себя. В вашей миссии по высушиванию этого кода вы рано или поздно найдете компоненты более высокого порядка, которые помогут вам извлечь и абстрагировать определенные проблемы компонента. С точки зрения разработки программного обеспечения компоненты высшего порядка являются формой шаблона декоратора. Компонент высшего порядка (HOC) — это, по сути, функция, которая принимает компонент React в качестве параметра и возвращает другой компонент React. Взгляните на следующий пример:

 import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; export default function requiresAuth(WrappedComponent) { class AuthenticatedComponent extends Component { static propTypes = { user: PropTypes.object, dispatch: PropTypes.func.isRequired }; componentDidMount() { this._checkAndRedirect(); } componentDidUpdate() { this._checkAndRedirect(); } _checkAndRedirect() { const { dispatch, user } = this.props; if (!user) { dispatch(push('/signin')); } } render() { return ( <div className="authenticated"> { this.props.user ? <WrappedComponent {...this.props} /> : null } </div> ); } } const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; AuthenticatedComponent.displayName = `Authenticated(${wrappedComponentName})`; const mapStateToProps = (state) => { return { user: state.account.user }; }; return connect(mapStateToProps)(AuthenticatedComponent); } 

Функция WrappedComponent получает компонент ( WrappedComponent ) в качестве параметра, который будет украшен требуемой функциональностью. Внутри этой функции класс AuthenticatedComponent визуализирует этот компонент и добавляет функциональность для проверки присутствия пользователя, в противном случае происходит перенаправление на страницу входа. Наконец, этот компонент подключается к хранилищу Redux и возвращается. Redux полезен, в этом примере, но не абсолютно необходим.

Подсказка : приятно установить displayName компонента в нечто вроде functionality(originalcomponentName) чтобы, когда у вас было много декорированных компонентов, вы могли легко различать их в отладчике.

4. Функция как дети

Создание складной строки таблицы не очень простая задача. Как вы визуализируете кнопку свертывания? Как мы будем отображать детей, когда стол не свернут? Я знаю, что с JSX 2.0 все стало намного проще, так как вы можете вернуть массив вместо одного тега, но я остановлюсь на этом примере, так как он иллюстрирует хороший пример использования функции Function как дочернего шаблона. Представьте себе следующую таблицу:

 import React, { Component } from "react"; export default class Table extends Component { render() { return ( <table> <thead> <tr> <th>Just a table</th> </tr> </thead> {this.props.children} </table> ); } } 

И складной корпус стола:

 import React, { Component } from "react"; export default class CollapsibleTableBody extends Component { constructor(props) { super(props); this.state = { collapsed: false }; } toggleCollapse = () => { this.setState({ collapsed: !this.state.collapsed }); }; render() { return ( <tbody> {this.props.children(this.state.collapsed, this.toggleCollapse)} </tbody> ); } } 

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

 <Table> <CollapsibleTableBody> {(collapsed, toggleCollapse) => { if (collapsed) { return ( <tr> <td> <button onClick={toggleCollapse}>Open</button> </td> </tr> ); } else { return ( <tr> <td> <button onClick={toggleCollapse}>Closed</button> </td> <td>CollapsedContent</td> </tr> ); } }} </CollapsibleTableBody> </Table> 

Вы просто передаете функцию как потомок, которая вызывается в функции render компонента. Вы, возможно, также видели эту технику, называемую «обратным вызовом рендеринга» или в особых случаях, как «рендер реквизита».

5. Рендеринг реквизита

Термин «рендер проп» был придуман Майклом Джексоном, который предположил, что шаблон компонентов более высокого порядка можно заменить 100% времени обычным компонентом «рендер проп» . Основная идея здесь — передать компонент React в вызываемую функцию как свойство и вызвать эту функцию в функции рендеринга.

Посмотрите на этот код, который пытается обобщить, как извлечь данные из API:

 import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class Fetch extends Component { static propTypes = { render: PropTypes.func.isRequired, url: PropTypes.string.isRequired, }; state = { data: {}, isLoading: false, }; _fetch = async () => { const res = await fetch(this.props.url); const json = await res.json(); this.setState({ data: json, isLoading: false, }); } componentDidMount() { this.setState({ isLoading: true }, this._fetch); } render() { return this.props.render(this.state); } } 

Как вы можете видеть, есть свойство с именем render , которое вызывается во время процесса рендеринга. Функция, вызываемая внутри него, получает полное состояние в качестве своего параметра и возвращает JSX. Теперь посмотрим на следующее использование:

 <Fetch url="https://api.github.com/users/imgly/repos" render={({ data, isLoading }) => ( <div> <h2>img.ly repos</h2> {isLoading && <h2>Loading...</h2>} <ul> {data.length > 0 && data.map(repo => ( <li key={repo.id}> {repo.full_name} </li> ))} </ul> </div> )} /> 

Как видите, параметры data и isLoading деструктурированы из объекта состояния и могут использоваться для управления ответом JSX. В этом случае, пока обещание не выполнено, отображается заголовок «Загрузка». Вам решать, какие части состояния вы передаете в рендер, и как вы используете их в своем пользовательском интерфейсе. В целом, это очень мощный механизм для извлечения общего поведения. Шаблон « Функции как дети» , описанный выше, в основном является тем же шаблоном, где свойство является children .

Подсказка : поскольку шаблон рендеринга реквизита является обобщением шаблона « Функция как дочерний», ничто не помешает вам иметь несколько реквизитов рендеринга в одном компоненте. Например, компонент Table может получить реквизит рендеринга для заголовка, а затем еще один для тела.

Давайте продолжим дискуссию

Надеюсь, вам понравился этот пост об архитектурных шаблонах React. Если вам что-то не хватает в этой статье (есть определенно больше лучших практик), или если вы просто хотите связаться, пожалуйста, напишите мне в Twitter .

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