Создание масштабируемой архитектуры CSS с помощью BEM и служебных классов

Создание масштабируемой архитектуры CSS с помощью BEM и служебных классов

От автора: поддерживать крупномасштабный CSS-проект сложно. За эти годы мы стали свидетелями введения различных подходов, направленных на облегчение процесса написания масштабируемого CSS.

В конце концов, мы все стараемся достичь следующих двух целей:

Эффективность: мы хотим сократить время, затрачиваемое на продумывание того, как все должно быть сделано, и увеличить время на то, чтобы сделать это.

Согласованность: мы хотим обеспечить, чтобы все разработчики исходили из того же.

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

Проблемы этого проекта заставили нашу команду разработать способ построения масштабируемой архитектуры CSS. Этот метод опирается на глобалы CSS, BEM и служебные классы. Я рад поделиться этим с вами!

CSS-глобалы за 30 секунд

Глобалы — это CSS-файлы, содержащие правила, которые применяются ко всем компонентам (например, масштабы интервалов, масштабы типографики, цвета и т. д.). Глобальные переменные используют токены, чтобы поддерживать согласованность дизайна для всех компонентов и уменьшить размер их CSS.

Вот пример глобальных правил типографики:

/* Typography | Global */
:root { /* body font size */ --text-base-size: 1em;

 /* type scale */ --text-scale-ratio: 1.2; --text-xs: calc((1em / var(--text-scale-ratio)) / var(--text-scale-ratio)); --text-sm: calc(var(--text-xs) * var(--text-scale-ratio)); --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio)); --text-lg: calc(var(--text-md) * var(--text-scale-ratio)); --text-xl: calc(var(--text-lg) * var(--text-scale-ratio)); --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
}


@media (min-width: 64rem) { /* responsive decision applied to all text elements */ :root { --text-base-size: 1.25em; --text-scale-ratio: 1.25; }
}


h1, .text-xxl { font-size: var(--text-xxl, 2.074em); }
h2, .text-xl { font-size: var(--text-xl, 1.728em); }
h3, .text-lg { font-size: var(--text-lg, 1.44em); }
h4, .text-md { font-size: var(--text-md, 1.2em); }
.text-base { font-size: 1em; }
small, .text-sm { font-size: var(--text-sm, 0.833em); }
.text-xs { font-size: var(--text-xs, 0.694em); }

БЭМ за 30 секунд

БЭМ (блоки, элементы, модификаторы) — это методология именования, предназначенная для создания повторно используемых компонентов. Вот пример:

<header class="header"> <a href="#0" class="header__logo"><!-- ... --></a> <nav class="header__nav"> <ul> <li><a href="#0" class="header__link header__link--active">Homepage</a></li> <li><a href="#0" class="header__link">About</a></li> <li><a href="#0" class="header__link">Contact</a></li> </ul> </nav>
</header>

Блок представляет собой многократно используемый компонент

Элемент является потомком блока (например, .block__element)

Модификатор представляет собой вариант блока / элемента (например, .block—modifier, .block__element—modifier).

Служебные классы за 30 секунд

Служебный класс — это класс CSS, предназначенный для выполнения только одной задачи. Например:

<section class="padding-md"> <h1>Title</h1> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</section>


<style> .padding-sm { padding: 0.75em; } .padding-md { padding: 1.25em; } .padding-lg { padding: 2em; }
</style>

Вы можете потенциально собрать из служебных классов целые компоненты:

<article class="padding-md bg radius-md shadow-md"> <h1 class="text-lg color-contrast-higher">Title</h1> <p class="text-sm color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</article>

Вы можете подключить служебные классы к CSS-глобалам:

/* Spacing | Global */
:root { --space-unit: 1em; --space-xs: calc(0.5 * var(--space-unit)); --space-sm: calc(0.75 * var(--space-unit)); --space-md: calc(1.25 * var(--space-unit)); --space-lg: calc(2 * var(--space-unit)); --space-xl: calc(3.25 * var(--space-unit));
} /* responsive rule affecting all spacing variables */
@media (min-width: 64rem) { :root { --space-unit: 1.25em; /* this responsive decision affects all margins and paddings */ }
}

/* margin and padding util classes - apply spacing variables */
.margin-xs { margin: var(--space-xs); }
.margin-sm { margin: var(--space-sm); }
.margin-md { margin: var(--space-md); }
.margin-lg { margin: var(--space-lg); }
.margin-xl { margin: var(--space-xl); } .padding-xs { padding: var(--space-xs); }
.padding-sm { padding: var(--space-sm); }
.padding-md { padding: var(--space-md); }
.padding-lg { padding: var(--space-lg); }
.padding-xl { padding: var(--space-xl); }

Практический пример

Объяснение методологии с помощью базовых примеров не описывает ни реальных проблем, ни преимуществ самого метода. Давайте создадим что-то реальное! Мы создадим галерею карточных элементов. Во-первых, мы сделаем это, используя только подход BEM, и укажем на проблемы, с которыми вы можете столкнуться, перейдя только на BEM. Далее мы рассмотрим, как Глобалы уменьшают размер CSS. Наконец, мы сделаем компонент настраиваемым, добавив в него служебные классы.

Вот конечный результат:

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

<div class="grid"> <article class="card"> <a class="card__link" href="#0"> <figure> <img class="card__img" src="/image.jpg" alt="Image description"> </figure>

 <div class="card__content"> <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>

 <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p> </div>

 <div class="card__icon-wrapper" aria-hidden="true"> <svg class="card__icon" viewBox="0 0 24 24"><!-- icon --></svg> </div> </a> </article>

 <article class="card"><!-- card --></article> <article class="card"><!-- card --></article> <article class="card"><!-- card --></article>
</div>

В этом примере у нас есть два компонента: .grid и .card. Первый используется для создания макета галереи. Второй — компонента карточки.

Прежде всего, позвольте мне указать основные преимущества использования BEM: низкая специфичность и область применения.

/* without BEM */
.grid {}
.card {}
.card > a {}
.card img {}
.card-content {}
.card .title {}
.card .description {}


/* with BEM */
.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title {}
.card__description {}

Если вы не используете BEM (или подобный метод именования), вы в конечном итоге создаете отношения наследования (.card > a).

/* without BEM */
.card > a.active {} /* high specificity */


/* without BEM, when things go really bad */
div.container main .card.is-featured > a.active {} /* good luck with that */


/* with BEM */
.card__link--active {} /* low specificity */

Работа с наследованием и спецификой в больших проектах болезненна. Это чувство, когда ваш CSS не работает, и вы обнаруживаете, что он был переписан другим классом class! BEM, с другой стороны, создает некую область видимости для компонентов и сохраняет специфичность низкой.

Но … есть два основных недостатка использования только БЭМ:

Именование слишком большого количества вещей разочаровывает

Нелегко выполнять незначительные настройки или поддерживать

В нашем примере для стилизации компонентов мы создали следующие классы:

.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title-wrapper {}
.card__title {}
.card__description {}
.card__icon-wrapper {}
.card__icon {}

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

<div class="card__content"> <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1> <p class="card__description">Lorem ipsum dolor...</p> <p class="card__description card__description--small">Lorem ipsum dolor...</p>
</div>

Как вы это назовете? Вы могли бы рассматривать это, как изменение элемента .card__description и выбрать .card__description — .card__description—small. Или вы можете создать новый элемент, что-то вроде .card__small, .card__small-p, или .card__tag. Понимаете, к чему я клоню? Никто не хочет тратить время на размышления об именах классов. БЭМ великолепен, если вам не нужно называть слишком много вещей.

Второй вопрос касается мелких настроек. Например, представьте, что вам нужно создать вариант компонента карточки, в котором текст будет выровнен по центру. Вы, вероятно, сделаете что-то вроде этого:

<div class="card__content card__content--center"> <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1> <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>


<style> .card__content--center { text-align: center; }
</style>

Один из ваших товарищей по команде, работающий над другим компонентом (.banner), сталкивается с той же проблемой. Он также создает вариации для своего компонента:

<div class="banner banner--text-center"></div>


<style> .banner--text-center { text-align: center; }
</style>

Теперь представьте, что вам нужно включить компонент banner на странице. Вам нужен вариант, где текст выравнивается по центру. Не проверяя CSS компонента banner, вы можете инстинктивно написать в HTML что-то похожее на banner banner—center, потому что вы всегда используете —center при создании вариантов, где текст выравнивается по центру. Не подходит! Единственный вариант — открыть CSS-файл компонента баннера, проверить код и выяснить, какой класс следует применять для выравнивания текста по центру.

Сколько времени это займет, 5 минут? Умножьте 5 минут на количество раз в день, которое вы и все ваши товарищи по команде вынуждены этим заниматься, и вы поймете, сколько времени потрачено впустую. Кроме того, добавление новых классов, которые делают то же самое, способствует раздутию CSS.

CSS-глобалы и служебные классы в помощь

Первое преимущество установки глобальных стилей заключается в наличии набора правил CSS, которые применяются ко всем компонентам.

Например, если мы установим адаптивные правила в глобальных пространствах интервалов и типографики, эти правила также затронут компоненты сетки и карточки. В CodyFrame мы увеличиваем размер основного шрифта в определенной контрольной точке; поскольку мы используем единицы «em» для всех полей и отступов, вся система интервалов обновляется сразу, генерируя каскадный эффект.

Как следствие, в большинстве случаев вам не нужно использовать медиа-запросы для увеличения размера шрифта или значений полей и отступов!

/* without globals */
.card { padding: 1em; }


@media (min-width: 48rem) { .card { padding: 2em; } .card__content { font-size: 1.25em; }
}


/* with globals (responsive rules intrinsically applied) */
.card { padding: var(--space-md); }

Но это еще не все! Вы можете использовать глобальные переменные для хранения поведенческих компонентов, которые можно комбинировать со всеми другими компонентами. Например, в CodyFrame мы определяем класс .text-component, который используется как «оболочка текста». Он заботится о высоте строки, вертикальном интервале, базовом стиле и других вещах.

Если мы вернемся к нашему примеру с карточкой, элемент .card__content можно заменить следующим:

<!-- without globals -->
<div class="card__content"> <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1> <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>


<!-- with globals -->
<div class="text-component"> <h1 class="text-lg"><span class="card__title">Title of the card</span></h1> <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>

Текстовый компонент позаботится о форматировании текста и сделает его согласованным во всех текстовых блоках проекта. Кроме того, мы уже удалили пару классов BEM.

Наконец, давайте познакомимся со служебными классами!

Служебные классы особенно полезны, если вы хотите позже настроить компонент без необходимости проверять его CSS.
Вот как изменится структура компонента карточки, если мы поменяем некоторые классы BEM на служебные классы:

<article class="card radius-lg"> <a href="#0" class="block color-inherit text-decoration-none"> <figure> <img class="block width-100%" src="image.jpg" alt="Image description"> </figure>

 <div class="text-component padding-md"> <h1 class="text-lg"><span class="card__title">Title of the card</span></h1> <p class="color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p> </div>

 <div class="card__icon-wrapper" aria-hidden="true"> <svg class="icon icon--sm color-white" viewBox="0 0 24 24"><!-- icon --></svg> </div> </a>
</article>

Количество классов BEM (компонентов) сократилось с 9 до 3:

.card {}
.card__title {}
.card__icon-wrapper {}

Это означает, что вы не будете иметь дело с именованием. Тем не менее, мы не можем полностью избежать проблемы с именами: даже если вы создаете компоненты Vue / React / SomeOtherFramework из служебных классов, вам все равно придется именовать компоненты.

Все остальные классы BEM были заменены служебными классами. Что если вам нужно сделать вариант карточки с большим заголовком? Замените text-lg на text-xl. Что делать, если вы хотите изменить цвет иконки? Замените color-white на color-primary. Как насчет выравнивания текста по центру? Добавьте к элементу text-component text-center. Меньше времени на размышления, больше времени на работу!

Почему бы нам просто не использовать служебные классы?

Служебные классы ускоряют процесс проектирования и упрощают настройку. Так почему бы не забыть о BEM и не использовать только служебные классы? Две основные причины:

Используя БЭМ вместе с служебными классами, HTML легче читать и настраивать. Используйте BEM для:

DRY HTML из CSS, который вы не планируете настраивать (например, поведенческие CSS-подобные переходы, позиционирование, эффекты наведения / фокусировки),

продвинутой анимации / эффектов.

Используйте служебные классы для:

«настраиваемых» свойств, часто используемых для создания вариаций компонентов (например, отступы, поля, выравнивание текста и т. д.),

элементов, которые трудно идентифицировать с новым значимым именем класса (например, вам нужен родительский элемент с position: relative→ создаем <div class=»position-relative»><div class=»my-component»></div></div>).

Пример:

<!-- use only Utility classes -->
<article class="position-relative overflow-hidden bg radius-lg transition-all duration-300 hover:shadow-md col-6@sm col-4@md"> <!-- card content -->
</article>


<!-- use BEM + Utility classes -->
<article class="card radius-lg col-6@sm col-4@md"> <!-- card content -->
</article>

По этим причинам мы рекомендуем не добавлять правило !important в служебные классы. Использование служебных классов не должно быть похоже на использование молотка. Как вы думаете, было бы полезно получать доступ и изменять свойство CSS в HTML? Используйте служебный класс. Вам необходимы несколько правил, которые не нужно редактировать? Напишите их в CSS. Этот процесс не обязательно должен быть идеальным с первого раза: вы можете настроить компонент позже, если потребуется. Может показаться трудоемким «получить решение», но это довольно просто, когда вы применяете все на практике.

Служебные классы не являются вашим лучшим союзником в создании уникальных эффектов / анимации

Подумайте о работе с псевдо-элементами или создании уникальных эффектов движения, которые требуют пользовательских кривых Безье. Для этого вам все равно нужно открыть файл CSS.

Рассмотрим, например, эффект анимированного фона разработанной нами карточки. Насколько сложно было бы создать такой эффект с помощью служебных классов? То же самое касается анимации иконок, для работы которой необходимы ключевые кадры анимации:

.card:hover .card__title { background-size: 100% 100%;
}


.card:hover .card__icon-wrapper .icon { animation: card-icon-animation .3s;
}


.card__title { background-image: linear-gradient(transparent 50%, alpha(var(--color-primary), 0.2) 50%); background-repeat: no-repeat; background-position: left center; background-size: 0% 100%; transition: background .3s;
}


.card__icon-wrapper { position: absolute; top: 0; right: 0; width: 3em; height: 3em; background-color: alpha(var(--color-black), 0.85); border-bottom-left-radius: var(--radius-lg); display: flex; justify-content: center; align-items: center;
}


@keyframes card-icon-animation { 0%, 100% { opacity: 1; transform: translateX(0%); } 50% { opacity: 0; transform: translateX(100%); } 51% { opacity: 0; transform: translateX(-100%); }
}

Конечный результат

Вот окончательная версия галереи карточек. Она также включает в себя служебные классы сетки для настройки макета.

Файловая структура

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

project/
└── main/ ├── assets/ │ ├── css/ │ │ ├── components/ │ │ │ ├── _card.scss │ │ │ ├── _footer.scss │ │ │ └── _header.scss │ │ ├── globals/ │ │ │ ├── _accessibility.scss │ │ │ ├── _breakpoints.scss │ │ │ ├── _buttons.scss │ │ │ ├── _colors.scss │ │ │ ├── _forms.scss │ │ │ ├── _grid-layout.scss │ │ │ ├── _icons.scss │ │ │ ├── _reset.scss │ │ │ ├── _spacing.scss │ │ │ ├── _typography.scss │ │ │ ├── _util.scss │ │ │ ├── _visibility.scss │ │ │ └── _z-index.scss │ │ ├── _globals.scss │ │ ├── style.css │ │ └── style.scss │ └── js/ │ ├── components/ │ │ └── _header.js │ └── util.js └── index.html

Вы можете хранить CSS (или SCSS) каждого компонента в отдельном файле (и, необязательно, использовать плагины PostCSS для компиляции каждого нового файла /component/componentName.css в style.css). Вы можете организовывать глобалы по своему усмотрению; вы также можете создать один файл globals.css и избежать разделения глобалов в разных файлах.

Заключение

Работа над крупномасштабными проектами требует надежной архитектуры, если вы хотите открыть свои файлы спустя месяцы и не потеряться в них. Есть много методов, которые решают эту проблему (CSS-in-JS, utility-first, атомарный дизайн и т. д.)

Метод, которым я поделился с вами сегодня, основан на создании перекрестных правил (глобалов), использовании служебных классов для быстрой разработки и BEM для модульных (поведенческих) классов.

Автор: Sebastiano Guerriero

Источник: https://css-tricks.com

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