Зачем использовать в вёрстке компонентный подход? Разбираемся, как и зачем верстать интерфейсы, используя этот подход, какие параметры и проблемы важно учесть. Разберём азы, забежим вперёд и подробно погрузимся в тему. Кстати, эта статья написана на основе одного из уроков курса «Профессиональная вёрстка на HTML и CSS» Яндекс Практикума.
Там, мы погружаемся в вёрстку глубже. Учим современным стандартам написания HTML- и CSS-кода, рассказываем, как превращать дизайн-макеты в страницы сайта, создавать сайты под разные устройства и адаптировать их под людей с разными потребностями.
Как выглядит компонентный подход в деле
В процессе вёрстки более или менее сложного интерфейса в дизайне повторяются одни и те же элементы: кнопки, всплывающие окна, карточки, списки. Они выглядят одинаково на разных страницах сайта или просто в разных его зонах. Это и есть компонентный подход к интерфейсам.
Пользователи любят предсказуемый интерфейс. Когда они привыкнут к поведению какого-то элемента, им будет проще использовать аналогичный в другом месте.
Как в этом примере:
При наведении на каждую клетку сайта caniuse.com появляется тултип с информацией. Каждая клетка ведёт себя одинаковым образом, а каждый тултип похож на другой. И клетка, и тултип — это компоненты интерфейса.
Понять разницу между HTML-элементом и компонентом проще всего, если считать, что компонент содержит в себе и структуру, и внешний вид, и функциональность. То есть компонент — это набор из HTML-, CSS- и иногда JavaScript-кода, который может быть использован повторно в разных контекстах.
Лэйаут и компоненты в компонентном подходе
Компонентный подход — самый распространённый взгляд на проектирование и разработку интерфейса. Он заключается в том, что при разработке страниц вы проводите мысленное разделение на лэйаут и компоненты, наполняющие его. Лэйаут — это каркас вашей страницы, её общая структура.
Так выглядит схематическое изображение лэйаута сайта, его структуры:
А вот что будет, если наполнить его компонентами:
Компоненты и элементы
Компоненты — это обычно набор элементов. Например, компонент «Форма» состоит из элементов: заголовок, поле ввода, текст лейбла и кнопка.
Но элементы внутри компонентов и сами могут быть компонентами. Например, кнопка может повторно использоваться в разных частях сайта.
На этой странице есть две очень похожие друг на друга кнопки:
Кнопка в шапке и кнопка в форме отличаются лишь размерами и цветом текста, но, скорее всего, у них разная функциональность. Эти особенности уступают перед одной важной деталью — таких кнопок в интерфейсе могут быть десятки, и все они будут примерно одинаковые. Поэтому лучше сделать один компонент «Кнопка» со всеми общими свойствами, а особенности поведения присвоить кнопкам-элементам, а не кнопкам-компонентам.
Элементы большого компонента — не всегда другие компоненты. Если какой-то элемент используется только в контексте конкретного компонента и, скорее всего, не будет использован нигде больше — это просто элемент.
Логотип справа из этого скриншота, скорее всего, встретится в интерфейсе не раз, а вот рука с указателем на него — это что-то специфичное для подвала сайта
Объёмы компонентов и их особенности
Компоненты бывают разные по объему. В одних может быть много элементов, а в других — всего один. Порой сложно решить, как делить интерфейс: на мелкие или на крупные детали.
Как делить интерфейс, решает разработчик. Принцип один — быстрее строить интерфейс из крупных строительных блоков, поэтому, как только видите что-то, что потенциально может быть объединено в конструкцию, подходящую для повторного использования, — объединяйте её в компонент. Мы умышленно пока не говорим, как это делать в коде. Сначала важно, чтобы вы поняли принцип стремления к повторному использованию разных частей интерфейса.
Примеры комплексных компонентов:
Шапка и подвал сайта будут использоваться на всех страницах. Как бы они ни были сложны — это компоненты.
Форма обратной связи может появляться на разных страницах — это компонент.
Карточка товара, статьи или услуги — это тоже компонент.
Всплывающее окно с разным содержимым — это компонент.
Входная секция с заголовком в статью — это компонент.
Примеры небольших компонентов:
Кнопки одинакового вида
Поля ввода
Иконки с одинаковыми свойствами
Заголовки с повторяющимися свойствами
Абзацы
Получается, что почти каждый отдельный HTML-элемент может быть компонентом, если его нужно повторно использовать с одинаковыми или очень похожими стилями.
Зачем выбирают компонентный подход
Разработка подавляющего числа продуктов ведётся в духе компонентного подхода. Как и самые популярные фреймворки и библиотеки строятся на его принципах. Разработка современного продуктового фронтенда в основном связана с такими технологиями, как React, Vue.js, Angular или Svelte, а это всё фреймворки с компонентным подходом.
Компонентный подход к разработке интерфейса — путь экономии в разработке. Собрав систему компонентов однажды, разработчики уменьшают рутину. Одни и те же кнопки, контроллы (элементы управления) и даже крупные блоки сайта не приходится писать заново. Они просто лежат в библиотеке и ждут, когда разработчик скопирует заготовку и вставит её в нужное место.
Компонентный подход — путь к осмысленному масштабированию проекта. Когда ваш сайт развивается, появляются новые разделы и функциональность. Собирать новый интерфейс гораздо проще из заготовок. С системой компонентов бизнес быстрее получает результат.
В конце концов компонентный подход позволяет наладить цикл непрерывных улучшений. При появлении библиотеки компонентов вы получаете «источник правды». Вы развиваете библиотеку, учитывая всё новые требования: доступности, бизнес-логики, функциональности интерфейса. После подключения библиотеки к сайту все обновления компонентов будут поступать оттуда.
За пределами вёрстки. Как компоненты работают в фреймворках
Перед тем как говорить о принципах вёрстки, позволяющих создавать компоненты, полезно увидеть практику применения компонентного подхода в его максимальном воплощении — в компонентных фреймворках. Это позволит сформировать картину результата, первым этапом получения которого является вёрстка.
Идеальный компонент — это набор разметки, стилей и функциональности, который можно переместить в любую новую зону одного проекта или даже в совершенно новый проект без изменения кода самого компонента.
В видео показано, как из одного проекта с именем react-project-1
копируют папку с компонентом во второй проект с именем react-project-2
. После этого остаётся только импортировать компонент в страницу, вставить в нужное место и дописать настройки, определённые в компоненте. На деле переиспользование в новом проекте выглядит похоже на HTML-теги, но вместо тегов используют целые компоненты. Вот такой код автор вставляет в новый проект:
<Accordition buttonText="Кто автор?">
Кен Кизи
</Accordition>
<Accordition buttonText="Что за книга">
«Пролетая над гнездом кукушки»
</Accordition>
Так на странице появляются два компонента с выпадающим по нажатии на кнопку текстом.
В других библиотеках повторное использование может быть ещё проще: в Vue.js или Svelte весь код компонента хранится в одном файле со специальным расширением. Этот файл содержит и стили, и разметку, и функциональность. Поэтому перетаскивать из проекта в проект приходится не папку с компонентом, а всего один файл.
А теперь представьте, что перетаскивать вообще ничего не нужно, весь код компонентов хранится где-то в сети, и вы только подключаете его оттуда в свои проекты. Обновления библиотеки компонентов также автоматически попадают к вам, вы только пользуетесь конструкциями вида:
<Accordition buttonText="Кто автор?">
Кен Кизи
</Accordition>
После этого они превращаются в интерфейс. Это реальность современной разработки. Одни люди составляют библиотеки компонентов, другие их используют.
Ограничения компонентного подхода в HTML и CSS
Разделение кода на компоненты — опция, которая лежит за пределами вёрстки на HTML и CSS. Если CSS-код можно разделить на сколь угодно малые части и подключить последовательно к HTML-файлу вашей страницы, то HTML-код воспроизводится браузером как монолит. То есть вся страница с HTML-кодом должна быть загружена браузером разом. В HTML нет возможности создать свои теги и компоненты без программирования.
Получается, что реализовать компоненты на чистом HTML и CSS невозможно. Нужны дополнительные программы, способные вставить отдельный кусок HTML-кода в общее полотно страницы. Такие программы называются сборщиками или бандлерами. Кроме сборщика для реализации компонента потребуется ещё одна программа — шаблонизатор. Она позволит создавать на HTML основу для контента, а сам контент помещать внутрь тегов при вызове компонента в нужном месте. Работает это примерно так:
<div class="component">
<img src="{image}" alt="{altText}">
<h2>{title}</h2>
<p>{text}</p>
</div>
Когда программа-шаблонизатор увидит конструкции в фигурных скобках, она поймет, что туда можно вставить содержимое с соответствующим именем. Часто это содержимое передается как атрибут компонента при вызове.
<component
image="/images/img.png"
altText="альтернативный текст"
title="Текст заголовка"
text="Описание"
/>
Существующие ограничения — это не повод забыть о компонентном подходе. Потому что идеи компонентного подхода должны влиять на то, как вы именуете ваши классы в HTML и какие решения принимаете при написании CSS-кода.
Как использовать компонентный подход в HTML и CSS
Все принципы вёрстки в духе компонентного подхода исходят из основной идеи: повторяющиеся в интерфейсе блоки кода нужно максимально подготовить к повторному использованию в любом контексте. На деле нужно уделить особое внимание трём параметрам: независимости от окружения, отсутствию внешней геометрии и позиционирования, адаптивности размеров.
Параметр 1. Независимость от окружения
Это означает, что в коде вашего компонента должно содержаться всё для его корректного отображения и работы. Предположим, вы описываете компонент карточки. В HTML он будет выглядеть примерно так:
<article class="card">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
Принцип независимости от окружения диктует, что все стили этой карточки должны быть описаны для класса card и потомков, не рассчитывая, что какие-то из стилей придут снаружи. А это, в свою очередь, означает и переопределение всех дефолтных стилей браузера внутри.
.card {
box-sizing: border-box;
font-family: Inter, sans-serif;
}
.card-image {
width: 100%;
}
.card-title {
margin: 0;
}
.card-text {
margin: 0;
}
В этом примере мы переопределили внутри компонента все стили, которые могли бы прийти снаружи, от браузера. Теперь компонент изолирован, он будет работать одинаково в любом контексте. Понятно, что стилей в карточке будет гораздо больше. Главное, что мы нивелировали влияние внешних факторов на этот компонент.
Несмотря на красоту идеи о полностью независимом компоненте, на деле писать так не всегда удобно. Разработчики часто сбрасывают браузерные стили глобально, задают шрифты для всего body
и делают прочие идущие вразрез с этой идеей вещи. Полная независимость от окружения — красивая концепция, вокруг которой стоит выстраивать диалог в команде. При этом вам самим нужно понимать границы доступного. Разные шрифты могут потребоваться в другом проекте, а вот остальная функциональность лучше бы была упакована внутри компонента.
Параметр 2. Отсутствие внешней геометрии и позиционирования
Компонент должен полностью определять своё внутреннее устройство, но не должен задавать никаких параметров за своими пределами. К внешней геометрии относят в первую очередь поля (margin
) и границы (border
). С границами всё просто: если назначить box-sizing: border-box
компоненту, границы перестанут влиять на внешнюю геометрию. С полями немного сложнее.
Карточек теперь будет две:
<div class="container">
<article class="card">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
<article class="card">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
</div>
Мы хотим задать первой карточке margin-bottom
, чтобы отодвинуть вторую. Этого не стоит делать для селектора .card.
Ведь в этом случае вторая карточка тоже получит этот нижний отступ.
Это очень простой пример, но с более сложными компонентами легко забыть, что все стили для компонента применятся в любом его контексте использования. Как же быть, если нужен отступ? Использовать ещё один класс или более сложный селектор, который конкретизирует контекст именно этой карточки.
Вот первый вариант:
<div class="container">
<article class="card mb">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
<article class="card">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
</div>
Мы задали первой карточке класс mb
, что значит margin-bottom
. Для него будем описывать отступы. Некоторые разработчики любят такой подход и создают целые системы отступов вроде mb-1
, mb-2
и т.д. Если классу mb-1
задать отступ в 8px
, mb-2
в таком случае задают 16px
. Получается система классов, созданная только для описания отступов. Такой подход практикуют в методологии «Atomic CSS».
Вот ещё один вариант:
<div class="container">
<article class="card container-card--starter">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
<article class="card">
<img src="/path-to-image.png" class="card-image" alt="текст">
<h2 class="card-title">Заголовок карточки</h2>
<p class="card-text">Абзац с описанием</p>
</article>
</div>
Идея та же самая — добавить дополнительный класс. Но за счёт имени класса container-card--starter
мы более понятно определяем, что эта карточка является не только самостоятельным компонентом, но и элементом блока container, причем тем, с которого он начинается. Этот подход ближе к методологии БЭМ (Блок, Элемент, Модификатор), идея которого в том, чтобы именами классов показывать принадлежность компонентов друг к другу.
Ещё неплохой вариант на CSS:
.container .card:first-of-type {
margin-bottom: 8px;
}
Таким кодом мы описали, что первому элементу с классом card, вложенному в контейнер container, нужно задать отступ. Этот вариант тоже связан с определением контекста использования компонента через его родительский блок, работа идёт с контекстом, а не с компонентом.
Плохой вариант:
.card:first-of-type {
margin-bottom: 8px;
}
Этот вариант плох, потому что вредит повторному использованию. В другом месте, где карточки встанут в ряд, а не одна под другой, этот отступ будет не нужен, но он появится.
С позиционированием ситуация похожа на внешние поля. Если вам нужно фиксированно, абсолютно или липко спозиционировать компонент — добавьте для этого отдельный класс. Всегда существует контекст, где компоненту позиционирование не нужно, и очень редко — когда нужно. Вы можете использовать те же приемы: внедрить систему классов для управления позиционированием, описать контекст в имени дополнительного класса или использовать CSS для выбора элемента через родителя.
Параметр 3. Адаптивность размеров
Правило простое: если ваш компонент блочный, то всегда нужно оставить его ширину в 100%
. Настройки размеров перенести в дополнительные классы. Это могут быть классы, описывающие контекст, как в примерах с полями и позиционированием, или дополнительные классы с модификацией размеров. Например, userpic_size_xl
или userpic_size_s
. Не задавайте размеры основным именам компонентов, размеры могут пригодиться разные.
Если ваш компонент ведёт себя строчно или блочно-строчно, управляйте размерами через внутренние свойства. Например, устанавливайте padding
у кнопок вместо ширины и высоты. Если вам нужны модификации размеров — также пользуйтесь дополнительными классами, а не основным. Если ваш компонент строчный или блочно-строчный, отдельно задумайтесь, должен ли он быть таким. Ссылки, скорее всего, должны оставаться строчными, но вот поля ввода и изображения, исходя из нашей практики, удобнее сразу делать блочными, чтобы расширить их возможности использования в качестве компонентов. За взаимное расположение элементов чаще всего отвечает лэйаут, а компоненты встраиваются в него в виде блоков.
Создаём файловые структуры в компонентном подходе
Некоторые методологии, например БЭМ, определяют, как раскладывать файлы. В React тоже есть принятые подходы. В команде вы можете просто следовать рекомендациям, а можете придумать своё разделение на компоненты.
Бывает полезно разделить вашу вёрстку на такие слои:
Лэйаут — общий каркас страниц.
Компоненты — большие структурные части.
UI — совсем небольшие строительные элементы.
В таком случае структура файлов CSS может приобретать подобный вид:
|-styles
--|-layouts
----|-index.css
----|-about.css
----|-contacts.css
----|-...
--|-components
----|-header.css
----|-footer.css
----|-cover.css
----|-...
--|-UI
----|-buttons.css
----|-social-icons-list.css
----|-inputs.css
----|-...
--|-global.css
Это просто пример разветвленной файловой структуры, в которой со временем может стать удобнее искать ваши стили.
Главная проблема при подобной сложной организации файловой структуры — это подключение стилей. При реализации сложных файловых структур вам придётся подключать все файлы в нужной последовательности внутрь тега <head>
каждой конкретной страницы или импортировать файлы CSS друг в друга через директиву @
import
.
Через специальные программы-сборщики можно автоматизировать подобный процесс, но настройка такой инфраструктуры у вас ещё впереди. Сейчас мы только хотим проблематизировать эту ситуацию. Файлы компонентов в больших проектах принято правильно организовывать.
Если возвращаться к использованию стилей в компонентных фронтенд-библиотеках и фреймворках, таких как React, там принят подход, когда каждому компоненту выдают отдельную папку внутри директории components
и в отдельной папке хранят весь код компонента. Примерно так:
|-components
--|-Accordition
----|-Accordition.jsx
----|-Accordition.css
Это не единственно возможное решение. В реальности файловых структур много и взглядов на архитектуру проектов тоже.
Подключаем внешние библиотеки компонентов
Создание компонентов и лэйаута и есть основная задача верстальщика. Компоненты разнятся от проекта к проекту. Чтобы повторно их использовать, нужно сначала их создать. Но иногда бывают задачи, не требующие уникального дизайна, но требующие скорости разработки. Самый частый пример — это админ-панели сайтов. Их видят только сотрудники какой-либо организации, там часто нет потребности в красоте или уникальности, нужно просто, чтобы всё работало и выглядело приемлемо. Для таких задач используют готовые библиотеки компонентов. Их уже написали другие разработчики, вам остаётся только подключить и использовать.
Самая известная и популярная библиотека — Bootstrap. К ней очень спорное отношение в сообществе, но это явный лидер по узнаваемости. Работает очень просто: подключаем файл CSS и cкрипт, заходим в раздел с компонентами, копируем HTML-код, всё работает, можно уделять внимание только контенту. Возможности достаточно богатые. Можно пользоваться сеткой, компонентами управления лэйаутом, а можно просто брать компоненты поменьше. Вот короткое видео с примером подключения и использования компонента карточки.
Подобных библиотек много, и работают они по похожему принципу. Вот ещё несколько более простых, чем Bootstrap:
С любой внешней библиотекой компонентов возникают со временем одни и те же проблемы: вам в какой-то момент захочется изменить дизайн под себя. В этом случае нужно будет писать стили поверх стилей библиотеки, а это не всегда просто.
Какие проблемы компонентного подхода до сих пор не решены
Компонентный подход и взгляд на интерфейс преобладает в современном вебе. CSS и браузеры адаптируются под веяния медленнее, чем рассчитывают многие разработчики. Поэтому у компонентного подхода есть «врождённые» проблемы, которые постепенно решаются.
Уровни переопределения стилей. Подключив библиотеку с компонентами, даже созданную своими руками, разработчик попадает в ситуацию, когда дополнительная стилизация поверх готовых компонентов усложнена. Стили бывают написаны так, что сложно перезаписать их. Подобную проблему видят и пытаются решить новым стандартом Cascade Layers. Это совсем новая спецификация, которую только начинают использовать в браузерах. Через неё разработчики смогут вмешиваться в то, как браузер приоритизирует применение стилей из разных файлов. Спецификация непростая для новичков, но со временем появится много примеров использования в практике.
Привязанность к контексту размера окна браузера. До последнего времени менять стили в компонентах можно было, только опираясь на размеры окна браузера, не на размеры блока, создающего контекст для компонента. Мы могли сказать: «Когда окно браузера станет шире, чем 450 пикселей, поменяй направление флекс-контейнера у компонента». Это не соответствует идеи независимости от окружения. Ведь компонент ведёт себя определенным образом, опираясь на контекст браузера, а не тот, в который оказался помещен. Было бы здорово уметь говорить компоненту: «Когда твой родитель станет меньше 600 пикселей в ширину, поменяй стили». Теперь так можно. Есть спецификация Container Queries. С её помощью можно будет управлять стилями, опираясь на контекст, куда встроен компонент.
Обе эти спецификации очень молодые, пока не годятся для массового использования. Нужно проверить их временем, но это революционные инструменты CSS, связанные с компонентным подходом.
За пределами CSS, в мире JavaScript, есть ещё одна технология, которая очень сильно связана с компонентным подходом, — Custom Elements. Это способ создавать свои элементы без каких-либо дополнительных инструментов (вроде React или Vue.js). С развитием и популяризацией этой технологии компонентный подход станет возможным в HTML без шаблонизаторов и сборщиков. Разработчики смогут создавать свои теги-компоненты. Но пока готовность к повседневному использованию этой технологии вызывает вопросы, а порог входа в разработку собственных компонентов достаточно высок.