Не может быть никаких сомнений в том, что 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.
Выбор одной из этих библиотек сильно зависит от вашего варианта использования:
- Вам нужна библиотека, чтобы выкладывать скомпилированный файл CSS для производства? EmotionJS может сделать это!
- У вас есть сложные темы? Стильные компоненты и Glamorous — ваши друзья!
- Нужно ли запускать на сервере? Это не проблема для последних версий всех библиотек!
Для 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 — это мощный и многогранный инструмент, который позволяет вам предоставлять пользователям возможности высокопроизводительного редактирования фотографий. Мы потратили годы на разработку лучшего решения и гордимся тем, чего достигли.