5 лучших практик в архитектуре React

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, поэтому этот вариант не подходит.

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

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

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

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

{ "main": "FilterSlider.jsx" } 

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

import FilterSlider from 'components/FilterSlider' 

2. CSS в JavaScript

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

Затем мы оценили некоторые из недавно появившихся на сцене решений CSS-in-JS:

Styled Components: самый популярный выбор с самым большим сообществом

EmotionJS: горячий конкурент

Glamorous: еще одно популярное решение CSS-in-JS.

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

Вам нужна библиотека, чтобы выплескивать скомпилированный файл CSS для продакшена? EmotionJS может это сделать!

У вас есть сложные проблемы с темой? Styled Components и Glamorous — ваши друзья!

Нужно что-то запустить на сервере? Это не проблема для последних версий всех библиотек!

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

Совет от про: при стилизации большого количества 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

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

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

Нам пришлось отобразить таблицу с несколькими складными элементами tbody.

Мы должны были отображать разные компоненты в зависимости от разных состояний.

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

Компоненты более высокого порядка (HOC)

Иногда вам нужно убедиться, что компонент React отображается только при входе пользователя в ваше приложение. Вначале вы проведете некоторые проверки работоспособности в методе render пока не обнаружите, что вы много повторяете. В своей миссии DRY над этим кодом вы рано или поздно найдете компоненты более высокого порядка, которые помогут вам извлечь и отвлечь некоторые проблемы компонента. Что касается разработки программного обеспечения, то компоненты более высокого порядка являются формой декоратора. Компонент более высокого порядка (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);
}

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

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

Функция в качестве детей

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

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 компонента. Возможно, вы также видели эту технику, называемую «обратным вызовом визуализации» или в особых случаях, как «render prop».

Render Props

Термин «render prop» был придуман Майклом Джексоном, который предположил, что образец компонента более высокого порядка можно всегда на 100% заменить на регулярный компонент с помощью «render prop» . Основная идея здесь — передать компонент 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. В этом случае, пока promise не был выполнен, отображается заголовок «Загрузка». Это зависит от вас, какие части состояния вы передаете в render prop и как вы используете их в своем пользовательском интерфейсе. В целом, это очень мощный механизм для извлечения общего поведения. Шаблон «Функция как дети», описанный выше, является в основном тем же шаблоном, где свойство является children.

Совет от про: Так как шаблон prop render является обобщением шаблона Function as children , вам нечего мешает иметь несколько render props на одном компоненте. Например, компонент Table может получить render prop для заголовка, а затем еще один для тела.

Автор: Sebastian Deutsch

Источник: https://www.sitepoint.com/

Редакция: Команда webformyself.