- PVSM.RU - https://www.pvsm.ru -
Вносить однотипные изменения в три-четыре разных места в JS-коде — искусство, требующее концентрации внимания. Если элементов больше, поддержка кода превращается в муку. Поэтому для долгосрочных или крупных проектов следует писать код так, чтобы его можно было вынести в отдельные компоненты.
Я занимаюсь фронтенд-разработкой уже 10 лет и расскажу о применении компонентов для создания элементов фронтенда — это значительно упрощает жизнь фронтенд-разработчика.
Написано при поддержке Mail.ru Cloud Solutions [1].
HTML-теги — условно «нулевой» уровень компонентов. У каждого из них свои функции и назначение.
CSS-классы — следующий уровень абстракции, на который обычно выходят при создании даже небольшого сайта. В правилах применения стилей к CSS-классу мы описываем поведение всех элементов, которые входят в условное подмножество элементов.
Правила, применяемые к CSS-классам, а также любым другим элементам, например HTML-тегам, позволяют централизованно задавать и изменять правила отображения любого количества однотипных элементов. Есть разные инструменты для работы со стилями элементов — собственно CSS, Sass, LESS, PostCSS, и методологии применения стилей — БЭМ, SMACSS, Atomic CSS, CSS Modules, Styled components.
Собственно компоненты — это:
Сейчас развивается технология Web Components, которая позволяет делать кастомные HTML-теги, включить в верстку шаблонные куски кода. Однако компоненты стали широко применяться благодаря современным фреймворкам фронтенд-разработки, таким как Angular, Vue, React. Возможности JavaScript позволяют легко подключить компонент:
import {Header, Footer} from "./components/common";
render() {
return (
...
)
}
Все крупные проекты приходят к своей библиотеке готовых компонентов либо к использованию одной из готовых. Вопрос, когда нужно переходить от копирования кода к созданию компонентов, решается индивидуально, здесь нет однозначных рецептов.
Стоит помнить не только о написании кода, но и его поддержке. Простым copy/paste однотипной верстки и изолированием стилей в CSS-классах можно какое-то время создавать отображение без особых рисков. Но если к каждому из элементов добавляется логика поведения, написанная на JS, выгоды от переиспользования кода ощущаются буквально со 2-3 элемента, особенно когда дело касается поддержки и модификации ранее написанного кода.
Предположим, наше приложение стало достаточно большим, и мы решили написать свою библиотеку компонентов. Предлагаю использовать для этого популярный инструмент фронтенд-разработки React. Одно из его преимуществ — возможность просто и эффективно использовать вложенные компоненты. В коде ниже старший компонент App использует три вложенных компонента: AppHeader, Article, AppFooter:
import React from "react";
import AppHeader from "./components/AppHeader";
import Article from "./components/Article";
import AppFooter from "./components/AppFooter";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
title : "My App",
contacts : "8 800 100 20 30"
firtsArticleTitle : "Welcome",
secondArticleTitle : "Let's speak about..."
}
};
render() {
return (
<>
<AppHeader
title={this.state.title}
/>
<Article
title={this.state.firstArticleTitle}
/>
<Article
title={this.state.secondArticleTitle}
/>
<AppFooter
contacts={this.state.contacts}
/>
</>
)
}
}
Обратите внимание: теперь не требуется использовать в верстке старший оборачивающий тег — обычно это был div
. Современный React предлагает инструмент Fragment, сокращенная запись которого <></>
. Внутри этих тегов можно использовать плоскую иерархию тегов, как в примере выше.
Мы использовали три библиотечных компонента, один из которых два раза в одном блоке. Данные из родительского приложения передаются в props компонента и будут доступны внутри него через свойство this.props
. Такой подход является типовым для React и позволяет быстро собирать отображение (View) из типовых элементов. Особенно если в вашем приложении много похожих страниц, отличающихся только содержимым статей (Model) и функциональностью.
Однако нам может потребоваться модифицировать библиотечный компонент. Например, одинаковые по функциональности компоненты могут отличаться не только текстовым содержимым, но и оформлением: цвет, отступы, границы. Также можно предусмотреть разные варианты функциональности одного и того же компонента.
Ниже рассмотрен такой случай: в зависимости от наличия коллбэка наш компонент может быть «отзывчивым» или оставаться просто View для отрисовки элемента на странице:
// App.js
...
render() {
return (
<Article
text={this.state.articleText}
onClick={(e) => this.bindTap(e)}
customClass={this.state.mainCustomClass}
/>
)
}
// Article.js
import React from "react";
export default class Article extends React.Component {
constructor(props) {
super(props);
};
render() {
let cName="default";
if (this.props.customClass) cName = cName + " " this.props.customClass;
let bgColor="#fff";
if (this.props.bgColor) bgColor = this.props.bgColor;
return (
{this.props.onClick &&
<div
className={cName}
onClick={(e) => this.props.onClick(e)}
style={{background : bgColor}}
>
<p>{this.props.text}<p/>
</div>
}
{!this.props.onClick &&
<div className={cName}>
<p>{this.props.text}<p/>
</div>
}
)
}
}
В React существует еще одна техника расширения возможности компонентов. В параметрах вызова можно передавать не только данные или коллбэки, но и целиком верстку:
// App.js
...
render() {
return (
<Article
title={this.state.articleTitle}
text={
<>
<p>Please read the article</p>
<p>Thirst of all, I should say programming React is a very good practice.</p>
</>
}
/>
)
}
// Article.js
import React from "react";
export default class Article extends React.Component {
constructor(props) {
super(props);
};
render() {
return (
<div className="article">
<h2>{this.props.title}</h2>
{this.props.text}
</div>
)
}
}
Внутренняя верстка компонента будет воспроизведена целиком так, как она была передана в props
.
Чаще удобнее передавать дополнительную верстку в библиотечный компонент с помощью паттерна «вставка» и использования this.props.children
. Такой подход лучше для модификации общих компонентов, отвечающих за типовые блоки приложения или сайта, где предполагается различное внутреннее наполнение: шапки, сайдбара, блока с рекламой и других.
// App.js
...
render() {
return (
<Article title={this.state.articleTitle}>
<p>Please read the article</p>
<p>First of all, I should say programming React is a very good practice.</p>
</Article>
)
}
// Article.js
import React from "react";
export default class Article extends React.Component {
constructor(props) {
super(props);
};
render() {
return (
<div className="article">
<h2>{this.props.title}</h2>
{this.props.children}
</div>
)
}
}
Выше были рассмотрены компоненты, которые отвечают только за View. Однако нам, скорее всего, потребуется выносить в библиотеки не только отображение, но и стандартную логику обработки данных.
Давайте рассмотрим компонент Phone, который предназначен для ввода номера телефона. Он может маскировать вводимый номер c помощью подключаемой библиотеки-валидатора и сообщать старшему компоненту, что телефон введен правильно или неправильно:
// Phone.js
import React from "react";
import Validator from "../helpers/Validator";
export default class Phone extends React.Component {
constructor(props) {
super(props);
this.state = {
value : this.props.value || "",
name : this.props.name,
onceValidated : false,
isValid : false,
isWrong : true
}
this.ref = React.createRef();
};
componentDidMount = () => {
this.setValidation();
};
setValidation = () => {
const validationSuccess = (formattedValue) => {
this.setState({
value : formattedValue,
isValid : true,
isWrong : false,
onceValidated : true
});
this.props.setPhoneValue({
value : formattedValue,
item : this.state.name,
isValid : true
})
}
const validationFail = (formattedValue) => {
this.setState({
value : formattedValue,
isValid : false,
isWrong : true,
});
this.props.setPhoneValue({
value : formattedValue,
item : this.state.name,
isValid : false
})
}
new Validator({
element : this.ref.current,
callbacks : {
success : validationSuccess,
fail : validationFail
}
});
}
render() {
return (
<div className="form-group">
<labeL htmlFor={this.props.name}>
<input
name={this.props.name}
id={this.props.name}
type="tel"
placeholder={this.props.placeholder}
defaultValue={this.state.value}
ref={this.ref}
/>
</label>
</div>
)
}
}
Этот компонент уже имеет внутреннее состояние state, частью которого он может делиться с вызвавшим его внешним кодом. Другая часть остается внутри компонента, в примере выше это onceValidated
. Таким образом, часть логики работы компонента заключена полностью в нем самом.
Можно сказать, что типовое поведение не зависит от других частей приложения. Например, в зависимости от того, был номер провалидирован или нет, мы можем показывать разные тексты подсказок. Мы вынесли в отдельный переиспользуемый компонент не только отображение, но и логику обработки данных.
Если наш типовой компонент поддерживает расширенную функциональность и имеет достаточно развитую логику поведения, то стоит разделить его на два:
Model
);View
).
Подключение будет происходить по-прежнему путем вызова одного компонента. Теперь это будет Model
. Вторая часть — View
— будет вызываться в render()
с props
, часть из которых пришла из приложения, а другая часть уже является state самого компонента:
// App.js
...
render() {
return (
<Phone
name={this.state.mobilePhoneName}
placeholder={"You mobile phone"}
/>
)
}
// Phone.js
import React from "react";
import Validator from "../helpers/Validator";
import PhoneView from "./PhoneView";
export default class Phone extends React.Component {
constructor(props) {
super(props);
this.state = {
value : this.props.value || "",
name : this.props.name,
onceValidated : false,
isValid : false,
isWrong : true
}
this.ref = React.createRef();
};
componentDidMount = () => {
this.setValidation();
};
setValidation = () => {
const validationSuccess = (formattedValue) => {
...
}
const validationFail = (formattedValue) => {
...
}
new Validator({
element : this.ref.current,
...
});
}
render() {
return (
<PhoneView
name={this.props.name}
placeholder={this.props.placeholder}
value={this.state.value}
ref={this.ref}
/>
)
}
}
// PhoneView.js
import React from "react";
const PhoneView = React.forwardRef((props, ref) => (
<div className="form-group">
<labeL htmlFor={props.name}>
<input
name={props.name}
id={props.name}
type="tel"
ref={ref}
placeholder={props.placeholder}
value={props.value}
/>
</label>
</div>
));
export default PhoneView;
Стоит обратить внимание на инструмент React.forwardRef()
. Он позволяет создавать ref
в компоненте Phone
, но привязывать его непосредственно к элементам верстки в PhoneView
. Все манипуляции как с обычным ref
будут в таком случае доступны в Phone
. Например, если нам нужно подключить валидатор номера телефона.
Другой особенностью такого подхода является максимальное упрощение View
компонента. Фактически эта часть определена как const, без своих встроенных методов. Только верстка и подстановка данных из модели.
Теперь наш переиспользуемый компонент разделен на Model
и View
, мы можем отдельно разрабатывать код бизнес-логики и верстки. Мы также можем собирать верстку из ещё более мелких компонентов-элементов.
Выше было показано, что приложение может управлять компонентами как с помощью передачи параметров или верстки, так и с помощью коллбэков.
Для успешной работы приложения требуется, чтобы верхний уровень получал значимые данные о состоянии вложенных переиспользуемых компонентов. При этом это может быть не самый верхний уровень всего приложения.
Если у нас есть блок авторизации клиента, и в нем переиспользуемые компоненты для ввода логина и пароля, всему приложению не надо знать, в каком состоянии находятся эти простые компоненты в каждый конкретный момент времени. Скорее, сам блок авторизации может вычислять новое состояние, основанное на состояниях простых переиспользуемых компонентов, и передавать его наверх: блок авторизации заполнен корректно или нет.
При большой вложенности компонентов требуется следить за организацией работы с данными, чтобы всегда знать, где находится «источник истины».
О некоторых трудностях, связанных с асинхронностью изменения состояния в React, я уже писал [2].
Переиспользуемые компоненты всегда должны передавать наверх через коллбэки данные, которые требуются для управления возможными блоками компонентов. Однако не нужно передавать лишние данные, чтобы не вызывать ненужных перерисовок больших частей DOM-дерева и не усложнять код обработки изменений в компонентах.
Другой подход к организации данных заключается в использовании контекста вызова компонентов. Это нативный метод React.createContext
, доступный с версии 16.3, не путать с более ранним React getChildContext
!..
Тогда не придется передавать props через «толщу» компонентов вниз по дереву вложенности компонентов. Либо использовать специализированные библиотеки управления данными и доставки изменений, такие как Redux и Mobx (см. статью о связке Mobx + React [3]).
Если мы построим библиотеку переиспользуемых компонентов на Mobx, у каждого из типов таких компонентов будет свой Store. То есть «источник истины» о состоянии каждого экземпляра компонента, со сквозным доступом из любого места во всем приложении. В случае с Redux и его единственным хранилищем данных все состояния всех компонентов будут доступны в одном месте.
Существуют популярные библиотеки готовых компонентов, которые, как правило, изначально были внутренними проектами компаний:
Все указанные библиотеки нацелены на построение верстки элементов и их стилизацию. Взаимодействие с окружением настраивают с помощью коллбэков. Поэтому если вы хотите создать полноценные переиспользуемые компоненты, описанные в третьем и четвертом пунктах статьи, придется сделать это самим. Возможно, взяв в качестве элементов View таких компонентов одну из популярных библиотек, представленных выше.
Статья написана при поддержке Mail.ru Cloud Solutions [1].
Что еще почитать: |
Автор: Максим
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/349233
Ссылки в тексте:
[1] Mail.ru Cloud Solutions: https://mcs.mail.ru/
[2] я уже писал: https://mcs.mail.ru/blog/razrabotka-na-react-kak-izbezhat-oshibok
[3] статью о связке Mobx + React: https://mcs.mail.ru/blog/plyusy-svyazki-mobx-react-dlya-upravleniya-sostoyaniem-prilozheniya
[4] Material-UI: https://material-ui.com/
[5] React-Bootstrap: https://react-bootstrap.github.io/
[6] VKUI: https://vkcom.github.io/vkui-styleguide/
[7] подробный материал по VK mini apps: https://mcs.mail.ru/blog/razrabotka-prilozhenij-v-vk-mini-apps
[8] ARUI Feather: https://github.com/alfa-laboratory/arui-feather
[9] может стать контрибьютором: https://github.com/alfa-laboratory/arui-feather/blob/master/.github/CONTRIBUTING.md
[10] Мой второй год в качестве независимого разработчика: https://habr.com/ru/post/487064/
[11] Полезные инструменты командной строки: https://mcs.mail.ru/blog/illyustrirovannoe-rukovodstvo-po-poleznym-instrumentam-komandnoj-stroki
[12] Как технический долг убивает ваши проекты: https://habr.com/ru/company/mailru/blog/486098/
[13] Источник: https://habr.com/ru/post/491174/?utm_source=habrahabr&utm_medium=rss&utm_campaign=491174
Нажмите здесь для печати.