# Препроцессор

# Простейший пример плохого кода

Безобразного кода с каждым днем становится все больше, несмотря на то, что больнее всего страдают от этого сами авторы, горе-разработчики. И если определенную функциональность или рендер можно покрыть тестами, то ошибки оформления выявляются только «ручками».

Также, просто парадоксально, что все программисты слышали о простых основополагающих принципах качественного программирования KISS и SOLID, но подавляющее большинство напрочь забывает о них, когда речь заходит об организации оформления, представления веб-интерфейса.

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

Давайте посмотрим на самый простейший пример плохого кода на CSS:

/* Примитивнейший пример обычного плохого кода на CSS */

/* Где-нибудь в файлах стилей: */

.selector--1 {
  width: 200px;
  height: 200px;
  border: 1px solid #ADADAD;
  border-radius: 3px;
  /* ... и дальше еще огромное количество самых разных правил */
}

.selector--2 {
  width: 200px;
  height: 400px;
  border: 1px solid #ADADAD;
  border-radius: 3px;
  /* ... и дальше еще огромное количество самых разных правил */
}

Не делайте так больше почти никогда! ))) Почему? Код валидный, «в браузере все по макету», да и все именно так обычно и пишут. Но все и не «верстают как бог», правильно? В контексте любого проекта чуть большего чем совсем крохотный, подобный код «плохой и чреват проблемами в будущем». Он конкретен и невыразителен, излишен, его сложно модифицировать и переиспользовать.

# А как надо?

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

Справедливости ради, нужно упомянуть, что последние годы, в связи с стремительным ростом популярности компонентных js-фреймворков и их подходов, все больше сторонников набирают также различные «CSS-in-JS»-реализации (например: Styled Components). Скоро, вероятно, можно будет спокойно использовать переменные в самом CSS (CSS Custom Properties). Тема холиварная, существуют контексты и ситуации когда подобный CSS-in-JS подход может оказаться более оправданным и изящным, без сомнения. И даже существует масса реалистичных кейсов когда проще всего будет действительно обойтись несколькими наборами правил на CSS, а любое его расширение будет излишним. Но в общем случае, в реальной коммерческой практике, имхо, для верстки сложных дизайнов и интерфейсов удобнее и эффективнее всего сейчас использовать любой препроцессор, и, шок - даже с компонентным фреймворком, дальше я планирую показать «как именно это лучше всего делать». Препроцессоры дают максимум возможностей и позволяют стремиться к максимальной выразительности и переиспользуемости. Вот во что превратился бы «плохой код» выше в SCSS-синтаксисе, наверное - самого популярного на сегодняшний день препроцессора - Sass:

// В @/src/scss/utils/_variables.scss:

$colors__border: #adadad;

$border-radius: 3px;


// В @/src/scss/utils/_placeholders.scss:
%border-block {
  border: 1px solid $colors__border;
  border-radius: $border-radius;
}


// В @/src/scss/utils/_mixins.scss:
@mixin size($width, $height) {
  width: $width;
  height: $height;
}


// В любом месте проекта:
.selector {
  $selector--1__size: 200px;
  $selector--2__width: 200px;
  $selector--2__height: 400px;

  &--1,
  &--2 {
    @extend %border-block;
    /* ... включение других сущностей препроцессора
      и специфическиих правил общих для селекторов */
  }

  &--1 {
    @include size($selector--1__size, $selector--1__size);
    /* ... включение других сущностей препроцессора
      и специфических правил уникальных для селектора */
  }

  &--2 {
    @include size($selector--2__width, $selector--2__height);
    /* ... включение других сущностей препроцессора
      и специфических правил уникальных для селектора */
  }
}

Точно тоже самое легко сделать и на, кажется, недооцененном, но очень удачном Stylus - совершенно не важно какой именно расширенный синтаксис вы используете, главное как и зачем. Очень много раз мне приходилось видеть плохой чужой код, написанный якобы для препроцессора, видимо, «потому что так сейчас модно», но, на самом деле, практически ничем не отличающийся от кода CSS. Не делайте так! Препроцессор дает нам крайне ценную возможность абстрагировать общие качества гайдлайна, стиль и основанные на нем частные стили, организовать их намного более выразительно и лаконично, легко модифицировать и переиспользовать при необходимости.

В данном, вырванном из контекста, но, при этом, вполне жизненном примере, кажется, что кода препроцессора - сильно больше. Он еще и раскидан по нескольким разным файлам, что, как будто, еще все усложняет. Зачем так напрягаться, а? Прежде всего, привычка начинать писать разметку с переменных и обобщений - очевидно грамотная. Перестаньте плодить изолированные глухие кряки с магическими числами, начните применять абстракцию! Делайте хорошо сразу, потому что вы почти никогда и ничего не переделаете «потом», на самом деле. «Когда наш стартап наконец взлетит», и как раз во многом из-за такого отношения он может и не взлететь, в результате. Чем детализированнее и сложнее ваш интерфейс, его дизайн, тем больше строк и времени вы будете «экономить» просто оптимизируя общие наборы правил, переиспользуя стили. Кроме того, поддерживать код и, тем более, вносить серьезные изменения будет на порядок проще.

Первый пример демонстрирует что на начальных этапах развития проекта хорошо продуманного кода препроцессора может быть даже визуально несколько больше, чем неорганизованного, внутренне плоского, скучного CSS. Но давайте разберем очень часто встречающийся кейс, в котором мы очевидно сразу сильно экономим много трафика и явно оптимизируем возможную поддержку. Такое очень часто встречается: нам нужно создать большое количество, предположим - 20 штук - модификаторов одного селектора - квадратной иконки размеров в 100 пикселей - и поставить в них нужные картинки в бекграунд. В Sass мы можем написать цикл с интерполяцией для создания селектора модификатора и указания пути до ресурса. И хотя такая синтаксическая возможность не является чем-то идейно решающе важным - на практике она экономит кучу времени и повышает качество жизни на работе:

// В @/src/scss/utils/_variables.scss:

// Paths
$images__path--root: "../../assets/images/";

// Sizes 
$icons__size: 100px;

// Views
$icons: 20;

// В любом месте проекта (в папке В @/src/scss/project/):
.icon {
  // корректируем путь до картинок
  $image-path: $image_path--root + "icons/";

  @include size($icons__size, $icons__size); // эта примесь уже создана выше

  @for $i from 1 through $icons {
    &.icon--#{$i} {
      background: url("#{$image-path}icon--#{$i}.svg") center center no-repeat;
    }
  }
}

Пример предполагает что в вашем проекте следующая структура:

.
└─ src
   ├─ assets
   │  └─ images
   │     └─ icons 
   │        ├─ icon--1.svg
   │        ├─ icon--2.svg
   │        └─ ...
   └─ sscs
      ├─ project
      │  └─ ...
      └─ utils
         ├─ _mixins.scss
         └─ _variables.scss

Теперь в шаблонах мы можем использовать:

<div class="icon icon--1"></div>

Если вы желаете чтобы картинки были с осмысленными именами - можете перебирать список:

.icon {
  $image-path: $image_path--root + "icons/";
  $images: "name1", "name2", "name3"; // Список имен

  @include size($icons__size, $icons__size);

  @each $image in $images {
    &.icon--#{$image} {
      background: url("#{$image-path}#{$image}.svg") center center no-repeat;
    }
  }
}

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

.selector {
  $width: 100px;

  width: calc(100vw - #{$width});
}

Используйте на полную мощность и изучайте подробно свои ключевые инструменты. Применяйте свой интеллект и фантазию. В хорошем коде препроцессора наборы правил не повторяются и какдый селектор содержит только небольшое количество объявлений. Повторяющиеся и похожие наборы, большое количество самых разных объявлений в селекторах - признак плохо организованного кода. Пока бесконечный поиск и замена остаются вашим главным и единственным инструментом рефакторинга кода разметки - вы живете в каменном веке веб-технологий и еще даже не начинали верстать!)

# Абстрагируй все!

Что такое дизайн, если совсем кратко? Дизайн - это «гайдлайн» - строгая система, набор стилевых правил и ограничений, перечень констант, аксиом и отношений в разметке и оформлении интерфейса, которым он неукоснительно должен соответствовать. Задача верстальщика в том чтобы правильно воспринять эту систему и максимально эффективно перевести ее с языка графических прототипов в работающий по заявленным требованиям код.

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

// В @/src/stylus/utils/variables.styl:

$colors = {
  mint: #44c6a8,
  // ... другие конкретные значения цветов
}

// Создаем "основной цвет", абстрагируясь от конкретного цвета
$colors['primary'] = $colors.mint
// ... другие "функциональные" цвета

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

.selector
  color $colors.primary

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

# Структура и стилевая база препроцессора

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

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

Но давайте уже организуем препроцессор, если с SCSS:

.
└─ src
   └─ sscs
      ├─ core // обшие и компилируемые сущности препроцессора
      │  ├─ _animations.scss // keyframes
      │  ├─ _base.scss // минимальная нормализация основных HTML-элементов
      │  ├─ _grid.scss // сетки
      │  ├─ _typography.scss // типографика
      │  └─ _utilities.scss // быстрые удобные классы-утилиты для включения прямо в разметку
      ├─ libraries // папка с файлами стилизаций сторонних модулей
      │  └─ _modal.scss - например какая-нибудь готовая модаль
      ├─ project // стили конкретного проекта
      │  ├─ _elements.scss // отдельные простые элементы-компоненты
      │  ├─ _fixes.scss // этот файл всегда должен быть практически пустой, и предназначен только для редких общеизвестных "собственных проблем браузеров"
      │  ├─ _layout.scss - стили общей для всех страниц GUI-обертки над контентом интерфейса
      │  └─ _widgets.scss - сложные составные комбинации простых элементов-компонентов
      ├─ utils // обшие и некомпилируемые основные сущности препроцессора
      │  ├─ _functions.scss // на практике нужны крайне редко
      │  ├─ _mixins.scss // параметризируемые и способные принимать контент примеси-микстуры
      │  ├─ _placeholders.scss // повторяющиеся наборы правил - растворы
      │  └─ _variables.scss // самый важный файл с переменными )
      ├─ _main.scss // точка сборки всех стилей препроцессора
      └─ _stylebase.scss // стилевая база

То есть, на самом деле - порядок сборки всей кухни имеет значение, конечно же:

// В @/src/scss/_stylebase.scss:
// Stylebase
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Uncompiled kitchen
@import "./utils/_functions";
@import "./utils/_variables";
@import "./utils/_mixins";
@import "./utils/_placeholders";

// Core base normal style and common utils
@import "./core/_animations";
@import "./core/_typography";
@import "./core/_base";
@import "./core/_grid";
@import "./core/_utilities";


// В @/src/scss/_main.scss:
// Project styles
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Stylebase for components
@import "_stylebase";

// App styles
@import "./project/_fixes";
@import "./project/_elements";
@import "./project/_widgets";
@import "./project/_layout";

// External libraries customization
@import "./libraries/_modal";

Итак, «стилевой базой» мы будем называть некое основное ядро стилей, доступный всем остальным компонентам системы общий код препроцессора. Более детально, он состоит из условно двух разных видов файлов:

  1. Растворяемые при компиляции инструменты-помощники, сущности позволяющие генерировать лаконичный, оптимальный, связный код:

    1. функции
    2. переменные
    3. параметризуемые примеси
    4. включения-плейсхолдеры
  2. Компилируемые глобальные стили:

    1. анимации keyframes
    2. типографика
    3. базовая нормализация основных HTML-элементов
    4. сетки
    5. утилитарные классы-помощники для разметки

В папки @/src/scss/project и @/src/scss/libraries вы можете добавлять файлы по необходимости.

Удобно держать подобную минимальную «кухню» наготове в ваших стартовых проектах, чтобы с самого начала работы быстро настраивать стилизацию под конкретные требования и гайдлайн.

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

# Адаптивная кухня

Стилевая база препроцессора призвана предоставлять все основные качества, константы гайдлайна и дизайна, например, такую важную его общую составляющую как типографика. Или, например - медиа-запросы для адаптивности в большинстве случаев используют одни и теже значения - брекпоинты - точки перехода, значения ширины окна браузера, при которых один статичный типоразмер дизайна перестраивается в другой. Признаком адаптивного дизайна как раз и является наличие нескольких таких статичных типоразмеров - диапазонов ширины экрана для которых не меняется пространственная раскладка, сетки и типографика на всем протяжении. Что, по сути, и позволяет дизайнерам ловко покрыть «все случаи» с помощью нескольких макетов - по одному на каждый типоразмер.

Нужно не забывать, что кроме нюансов при компиляции, плейсхолдеры, в отличие от параметризированных миксинов, не могут быть включены в медиа-запросы, но сами, при этом, могут их использовать. Давайте возьмем простейший обычный реалистичный кейс. Напишем стили препроцессора предоставляющие «резиновый контейнер для контента с отступами по краям вдвое меньшими на мобильных чем на остальных экранах». Обычно такой элемент не «обозначен явно», но на самом деле - подразумевается как обертка в любом адаптивном макете и часто переиспользуется по всему интерфейсу. Первым делом - объявляем переменную мобильного брекпоинта, создаем на ней удобную для использования стандартную примесь принимающую контент в медиа-запросе, используем эту адаптивную примесь в плейсхолдере для корректировки внутренних отступов, и в конце-концов - растворяем этот набор в конкретной утилите-классе. Только этот класс и будет скомпилирован в результате:

// В @/src/stylus/utils/variables.styl:
// Project variables
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Sizes
//////////////////////////////////////////////////////

$gutter = 20px


// Breakpoints
//////////////////////////////////////////////////////

$breakpoints = {
  tablet: 768px,
}

$breakpoints['mobile--max'] = $breakpoints.tablet - 1


// В @/src/stylus/utils/mixins.styl:
// Project mixins
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Media
//////////////////////////////////////////////////////
// Breakpoints in @/src/stylus/utils/variables.styl

$mobile()
  @media only screen and (max-width $breakpoints.mobile--max)
    {block}


// В @/src/stylus/utils/placeholders.styl:
// Project placeholders
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Rubber Container
$container
  width 100%
  padding-left $gutter
  padding-right $gutter

  +$mobile()
    padding-left $gutter / 2
    padding-right $gutter / 2


// В @/src/stylus/core/utilities.styl:
// Project utilities
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Elements
//////////////////////////////////////////////////////

// Rubber Container
.container-fluid
  @extends $container

Теперь у этих элементов - будут одинаковые правильные адаптивные отступы по краям:

<div id="element1">
    <div class="container-fluid"></div>
</div>

<div id="element2">
    <div class="container-fluid"></div>
</div>

<!-- Или можно даже так, но лучше, на самом деле, не надо: -->
<div id="element3" class="container-fluid"></div>

Код опять как будто выглядит излишне громоздким, «не проще ли было просто написать все с медиа-запросом в классе и готово». Нет, это обманчиво, и если вы все еще мыслите так, то вам стоит либо совсем перестать читать, либо перечитать все с самого начала. Я просто демонстрирую правильный ход мыслей и действий. Вы можете опираться на другие единицы измерения, написать все в каком-то немного другом синтаксисе, примеров в сети масса, но для быстрого и надежного адаптивного дизайна вам обязательно непреложно нужно «что-то такое», нечто, предоставляющее возможность воздействовать на оформление через систему брекпоинтов и типоразмеров. Особенно полезно принять такое соглашение и стандартизацию при работе над проектом командой. В масштабе большого проекта неукоснительное следование принципам и правилам становиться категорически важным.

Справедливости ради, нужно упомянуть что Custom Properties «не умеют быть брекпоинтами», что, кажется, их нелепый минус, но современные спецификации пространственной раскладки Grid и Flexbox умеют делать некоторые магические вещи с раскладкой даже без медиа-запросов. В любом случае, использование современных нативных сеток в связке с описанным здесь адаптивным препроцессором и кажется самым эффективным подходом.

На самом деле, нам осталось совсем немного для того чтобы «все понять про адаптивный дизайн». Давайте сделаем не «резиновый» контейнер - то есть тянущийся на всю доступную ширину, а с фиксированной для каждого типоразмера шириной. Пусть в нашем дизайне только три типоразмера: все лэптопы-ноуты, таблетки и мобилы. По сути это и будет самая простая основная «рамочная» конструкция любого «адаптивного» дизайна.

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

// В @/src/stylus/utils/variables.styl:
// Project variables
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Sizes
//////////////////////////////////////////////////////

$container__desktop = 1080px;
$container__tablet = 700px;


// Breakpoints
//////////////////////////////////////////////////////

$breakpoints = {
  tablet: 768px,
  desktop: 1240px,
}

$breakpoints['tablet--max'] = $breakpoints.desktop - 1


// В @/src/stylus/utils/mixins.styl:
// Project mixins
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Media
//////////////////////////////////////////////////////
// Breakpoints in @/stylus/utils/variables.styl

$desktop()
  @media only screen and (min-width $breakpoints.desktop)
    {block}

$gadgets()
  @media only screen and (max-width $breakpoints.tablet--max)
    {block}

$tablet()
  @media only screen and (min-width $breakpoints.tablet) and (max-width $breakpoints.tablet--max)
    {block}

$not-mobile()
  @media only screen and (min-width $breakpoints.tablet)
    {block}

$landscape()
  @media only screen and (orientation: landscape)
    {block}

$portrait()
  @media only screen and (orientation: portrait)
    {block}


// В @/src/stylus/core/utilities.styl:
// Project utilities
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// Elements
//////////////////////////////////////////////////////

// Rubber Container
.container
  @extends $container // раствор создан выше
  margin 0 auto // по центру

  +$desktop()
    max-width $container__desktop

  +tablet()
    max-width $container__tablet

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

Но, кроме этого, еще со времен когда я часто использовал Bootstrap 3, у меня остался формирующий удобные утилиты пассаж с обычно огульно осуждаемым, но на самом деле очень полезным в некоторых реальных ситуациях флагом !important [для простой быстрой стилизации сторонних модулей, которые выставляют стили инлайн через javascript, или тут, например - с ним надежнее]. Этот код предоставляет более «грубое и внешнее» решение - набор, созданных с помощью наших стандартных адаптивных примесей, утилитарных классов для принудительного включения прямо в разметку шаблонов:

// В @/src/stylus/core/utilities.styl:
// Visibility utilities
//////////////////////////////////////////////////////

// stylelint-disable declaration-no-important
.visible--desktop,
.visible--gadgets,
.visible--tablet,
.visible--mobile,
.visible--desktop--inline-block,
.visible--gadgets--inline-block,
.visible--tablet-inline-block,
.visible--mobile--inline-block,
.visible--desktop--inline,
.visible--gadgets--inline,
.visible--tablet--inline,
.visible--mobile--inline
  display none !important

.visible--desktop
  +$not-gadgets()
    display block !important

.visible--gadgets
  +$gadgets()
    display block !important

.visible--tablet
  +$sm()
    display block !important

.visible--mobile
  +$xs()
    display block !important

.visible--desktop--inline-block
  +$not-gadgets()
    display inline-block !important

.visible--gadgets--inline-block
  +$gadgets()
    display inline-block !important

.visible--tablet--inline-block
  +$sm()
    display inline-block !important

.visible--mobile--inline-block
  +$xs()
    display inline-block !important

.visible--desktop--inline
  +$not-gadgets()
    display inline !important

.visible--gadgets--inline
  +$gadgets()
    display inline !important

.visible--tablet--inline
  +$sm()
    display inline !important

.visible--mobile--inline
  +$xs()
    display inline !important

.hidden--desktop
  +$not-gadgets()
    display none !important

.hidden--gadgets
  +$gadgets()
    display none !important

.hidden--tablet
  +$sm()
    display none !important

.hidden--mobile
  +$xs()
    display none !important
// stylelint-enable declaration-no-important

Дизайнеры склонны мыслить «магически статично», они обусловлены рамками размера макета и пропорции в которой рисуют. И поэтому, например, часто выставляют принудительные переносы в тексте, хотя лучше этого не делать, позволяя тексту переноситься «нативно», и ограничивая его только по максимальной ширине - в реальности экраны очень разные. Но если вам необходимо стравиться с таким поведением - не стоит игнорировать проблему с пробелом, особенно если заголовок отбит центрально, и очень удобно применить адаптивные классы прямо в HTML:

<h2>Очень длинный и отбитый центрально заголовок<br class="visible--mobile--inline" /><span class="hidden--mobile"> </span>с принидительным переносом на мобильных</h2>

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

# Типографика

Спросите любого нормального дизайнера: «Что в стиле и оформлении веб-интерфейса (то есть в UI, а не в UI/UX) самое главное?». Думаю, многие ответят: «Типографика». Многим заказчикам и пользователям нравятся «рюшечки и котики», или - контент который предоставляет ваша верстка должен быть уверено доступен с клавиатуры без мыши, и, что очень важно, разметка должна быть дружественна для слабовидящих людей. Но все же, типографика и текстовый контент категорически важны, текстовые поля одного уровня в структуре документа и одной роли в дизайне должны быть с перманентно стабильным оформлением по всему интерфейсу, по крайней мере.

Сейчас мы организовали препроцессор, его стилевую базу и добавили в нее стандартный инструментарий для адаптивности. Давайте сделаем типографику, такую чтобы можно было как в истории рассказанной во вступлении к этому пособию «вообще все контролировать». Тут я тоже обычно использую подход «как в Bootstrap 3» на базовом кегле в пикселях [потому что в макетах оно именно в пикселях], предположим что у нас есть всего три основных кегля и два начертания шрифта, пишем на SCSS:

// В @/src/scss/core/_typography.scss:
// Typographic variables
//////////////////////////////////////////////////////

// Good line-height and letter-spacing
$line-height--base: 1.428571429;
$normal-letter-spacing: normal;

// Guide

$font-size--base: 16px;
$font-size__coefficient--large: 2.5;
$font-size__coefficient--normal: 1.5;
$font-size__coefficient--small: 1;

$font-size--large: round($font-size--base * $font-size__coefficient--large); // 40px
$font-size--normal: round($font-size--base * $font-size__coefficient--normal); // 24px
$font-size--small: round($font-size--base * $font-size__coefficient--small); // 16px

$line-height--computed: round($font-size--base * $line-height--base);
$line-height--large: floor($line-height--computed * $font-size__coefficient--large); // 55px
$line-height--normal: floor($line-height--computed * $font-size__coefficient--normal); // 35px
$line-height--small: floor($line-height--computed * $font-size__coefficient--small); // 23px

$font-family__sans: Helvetica, Arial, sans-serif;
$font-weight__sans__regular: 400;
$font-weight__sans__bold: 700;

Я думаю, тут все просто и понятно: мы используем базовый кегль, коэффициенты для основных кеглей из гайда, и стандартный множитель для интерлиньяжа, «хорошей высоты строки». И вот тут - давайте сделаем стандартную примесь которую будем использовать повсеместно. Для отдачи типографики абсолютно во все текстовые полях интерфейса:

// Universal Typographic Mixin
//////////////////////////////////////////////////////

// We use one, only one, Karl, a universal mixin for all cases !!!!!!!!!!

@mixin text($font-family, $font-size, $font-weight) {
  font-family: $font-family;
  font-size: $font-size;
  font-weight: $font-weight;
  letter-spacing: $normal-letter-spacing;

  @if $font-size == $font-size--large {
    line-height: $line-height--large;
  } @else if $font-size == $font-size--normal {
    line-height: $line-height--normal;
  } @else if $font-size == $font-size--small {
    line-height: $line-height--small;
  } @else {
    line-height: floor($font-size * $line-height--base);
  }
}

Теперь, если вам нужно, например, изменить какой-то стандартный кегль для определенного типоразмера (так очень часто бывает), вы можете сделать это для всего интерфейса с помощью включения адаптивной примеси прямо в «единую примесь для типографики». Просто добавим глобальную переменную и еще одно условие в примесь:

$font-size__coefficient--large--mobile: 2;

@mixin text($font-family, $font-size, $font-weight) {
  @if $font-size == $font-size--large {
    @include mobile {
      font-size: round($font-size--base * $font-size__coefficient--large--mobile); // 32px
      line-height: floor($line-height--computed * $font-size__coefficient--large--mobile);// 46px
    }
  }
}

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

// В любом месте проекта:
.selector {
  @include text($font-family_sans, $line-height_small, $font-weight_sans_bold);
  line-height: 17px;
}

Мы можем раздать стандартную типографику по заголовкам, в параграф, а также сделать утилиту для включения прямо в разметку:

// Base Typographic
//////////////////////////////////////////////////////

p {
  margin-bottom: $gutter / 2;
  @include text($font-family__sans, $font-size--small, $font-weight__sans__regular);

  // Можно применить адаптивную примесь чтобы изменить поведение более локально -
  // только для этого элемента на определенном типоразмере
  @include mobile {
    @include text($font-family__sans, $font-size--small * 1.2, $font-weight__sans__regular;
  }
}

h1, h2 {
  @include text($font-family__sans, $font-size--large, $font-weight__sans__bold);
}

h3, h4 {
  @include text($font-family__sans, $font-size--normal, $font-weight__sans__bold);
}

h5, h6 {
  @include text($font-family__sans, $font-size--small, $font-weight__sans__bold);
}

strong {
  font-weight: $font-weight__sans__bold;
}

.text {
  &--large {
    @include text($font-family__sans, $font-size--large, $font-weight__sans__bold);
  }

  &--normal {
    @include text($font-family__sans, $font-size--small, $font-weight__sans__bold);
  }

  &--small {
    @include text($font-family_sans_ui, $font-size_small, $font-weight_sans-ui_regular);
  }

  &--center {
    text-align: center;
  }
}

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

Здесь можно посмотреть как точно такая же управляемая минимальная типографическая кухня реализована на Stylus.

# Нормализация

В современных условиях, верстая только для modern bro, вы можете не заморачиваться каким-то специальным сбросом-нормалайзом и использовать для этих целей специальный файл стилевой базы, например на SCSS или на Stylus.

Содержание этого файла должно быть крайне минималистичным, очень аккуратным. Базовая нормализация основных HTML-элементов, стили для html и body, блоки. Нестандартное поведение. Какие-то специфические элементы, например - плейсхолдеры.

// No outline on focus controls!!!
button,
input,
textarea,
a {
  // stylelint-disable-next-line declaration-no-important
  outline: none !important;

  &:hover,
  &:active,
  &:focus {
    // stylelint-disable-next-line declaration-no-important
    outline: none !important;
  }
}

Правила здесь, это то что должно быть точно и вообще везде! Не наносите никакое оформление и кастомизацию с помощью этих правил (только на <body> и <html>)! Нормализуйте и готовьте!

# Стиль кода, номенклатура и композиция

Окончательно стало необходимо поговорить о том, о чем бы я поговорил с самого начала, если бы это был сухой технический гайд. О стиле кода, номенклатуре имен и композиции. И, прежде всего, в этом смысле, вместо того чтобы разводить бессмысленные холивары о «табах и пробелах», я советую просто использовать на любых проектах EditorConfig и линтеры. В контексте содержания этого раздела собственно stylelint. Да, линтеры это бесячие штуки, понятно, «нам все время не до этого», «баги горят», «боссы-заказчики рвут и мечут»... но, в реальности, только регулярная проработка линтером позволяют легко поддерживать ваш, например - общий командный код - в священной кристальной чистоте и красоте. Настроить Stylelint «под себя», это, на самом деле, совсем недолго - вечер, и даже по-своему интересно-занимательно, если вы цените свои инструменты. Сейчас я используют вот такой конфиг Stylelint для SCSS.

# Именование

Имена записываются в нижнем регистре. Наименование должно быть:

  • надежно уникальным;
  • осмысленным и легко интуитивно-понятным;
  • как можно более лаконичным, при этом.

При именовании селекторов по классу и идентификаторов, составных частей компонентов, элементов, блоков и их модификаторов, удобно применять формальный БЭМ-подход, стиль Two Dashes:

.block__element--modifier {
  // стили модифицированного элемента блока
}

Возможности сложной композиции, наличие каскадирования и Parent Selector (в SCSS, в Stylus) в синтаксисах препроцессоров к этому крайне располагает:

.block {
  // стили блока

  &__element {
    // стили элемента блока

    &--modifier {
      // стили модифицированного элемента блока
    }

    &--modifier & {
      &__element {
        // стили элемента внутри модифицированного блока
      }
    }
  }

  &--modifier {
    // стили модифицированного блока
  }
}

А вот так делать уже не рекомендуется:

.block {
  &__element {
    &-wrapper {
      // Сделать стили для элемента .block__element-wrapper
    }
  }
}

В этом случае в дальнейшем вы не сможете найти маркер element-wrapper поиском по проекту. С другой стороны если вы пишите строго компонентную систему и вас четко «все разложено по полочкам», можно не сильно переживать за поиск - «искать становиться нечего и негде» практически. Тогда иногда все же можно применять эту возможность как «быстрый фикс». Предположим вам понадобилось по-быстрому довернуть обертку элементу вокруг элемента - так бывает - заказчик внезапно захотел еще что-то или по другому чем было в макете, и для чтобы реализовать это просто необходима промежуточная обертка:

<div class="component">
    <div class="component__element-wrapper">
        <div class="component__element"></div>
    </div>
</div>

Кроме того, при необходимости можно связывать совсем абстрактно, «не по БЭМ» - дальше об этом будет подробнее в разделе про композицию:

.block {
  .wrapper & {
    // Сделать специфические стили для .block внутри .wrapper
  }
}

Но, на самом деле, полностью невозможно избежать появления в коде всевозможных простых имен-утилит, классов-модификаторов для логики или даже составных селекторов с голыми тагами. Например, все еще вы используете какой-нибудь громоздкий Bootstrap или другие сторонние модули поскромнее - такие имена будут пробираться в ваш код из этих библиотек и решений. Или просто, предположим - верстаете быстрый прототип.

.block.fade-on {
  // модификатор, но со стандартной неспецифической анимацией
}

.block .overlay {
  // простой переиспользуемый элемент-утилита внутри блока
}

.block ul > li {
  // быстрая конструкция для проверки гипотезы при прототипировании, например
}

Неоправданно раздутые стили, в которых множество селекторов описывают одно и то же, перегруженный классами HTML — в некоторых ситуация тоже может быть совсем не совсем уместным или даже явно неадекватным подходом (быстрый прототип, например). Если необходимо выразить уникальность и обеспечить железную изоляцию чего-либо - следует создавать селекторы по БЭМ; если что-то заурядно, единообразно повсюду, может быть переиспользовано или вероятно будет изменено — лучше не перегибать палку.

Предположим, нам необходимо разметить простую страницу-лендинг, содержащую два баннера-секции с несколькими простыми элементами внутри: заголовком, параграфом текста и CTA-кнопкой действия. В таком случае нам требуются всего три «БЭМ-идентификатора»: самого родительского «блока» - страницы, и двух его дочерних «элементов» - баннеров, во всех остальных случаях, для остальной разметки мы вполне можем обойтись семантикой голых тегов с готовой общей типографикой (h2, p) и простыми классом элементом общим для всего интерфейса (.button):

<main class="page page-name" role="main">

    <section class="banner page-name__banner--1">
        <h2>Заголовок 1</h2>
        <p>Текст 1 ...</p>
        <a href="#" class="button">Перейти</a>
    </section>

    <section class="banner page-name__banner--2">
        <h2>Заголовок 2</h2>
        <p>Текст 2 ...</p>
        <a href="#" class="button">Перейти</a>
    </section>

</main>

Код препроцессора:

// Дефолтная типографика проекта в @/src/scss/core/_typography.scss:

h2 {
  ...
}

p {
  ...
}

// Элементы проекта в @/src/scss/project/_elements.scss:

// Обычная кнопка
.button {
  ...
}

// Виджеты проекта в @/src/scss/project/_widgets.scss:

// Страницы - общее
.page {
  ...
}

// Баннеры - обшее
.banner {
  ...
}

// Конкретный макет - в @/src/scss/project/_page-name.scss:
.page-name {
 
  // Первый баннер макета
  &__banner--1 {
    ...
  }

  // Второй баннер макета
  &__banner--2 {
    ...
  }
}

Теперь мы можем уверенно влиять на поведение элементов и голых тегов при помощи всего трёх селекторов. Дальше мы еще вернемся к этому примеру, рассматривая возможности композиции, сейчас мы говорим о наименовании.

Также важными кажутся соглашения о выборе имен переменных и других сущностей препроцессора:

  • имена переменных чаще всего состоят из смысловых блоков-маркеров определяющих разные сущности которые контролирует переменная, для разделения таких частей используется двойное нижнее подчеркивание (_) или двойной дефис (--);
  • для разделения маркеров в имени переменной, обозначающих одну сущность, например, свойство CSS, используется дефис (-).
// В @/src/scss/utils/_variables.scss:
// Elements
//////////////////////////////////////////////////////

// Sizes and rounding
//////////////////////////////////////////////////////

$border-radius__small: 7px;

$controls__height: 70px;
$controls__height--mobile: 50px;


// Buttons

$buttons__border-radius: $border-radius__small;

$buttons__height: $controls__height;
$buttons__height--mobile: $controls__height--mobile;

Подобные подходы возможно применять и для выбора имен других сущностей препроцессора - растворов-плейсхолдеров и параметризированных примесей или утилит:

// В @/src/scss/utils/_variables.scss:

$colors__black: #000000;
$colors__white: #ffffff;


// В @/src/scss/utils/_placeholders.scss:

%background {
  &--black {background: $colors__black}

  &--white {background: $colors__white}
}


// В @/src/scss/utils/_utilities.scss:
.background {
  &--gray {@extend %background--black}
    
  &--white {@extend %background--white}
}

# Композиция

Невзирая на существующее сегодня в среде разработчиков серьезное предубеждение, я предлагаю активно, но при этом аккуратно, использовать удобный базовый функционал препроцессора — каскадирование. Если необходимо, и наглядно, в плане выразительности кода препроцессора в development-проекте, и технически, в плане содержания и надёжности скомпилированных стилей на production, обозначить зависимость чего-либо от чего-либо — можно просто вложить одно в другое.

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

# Естественные композиции и голая семантика

Конструкции которые в принципе нельзя ломать, например, списки и сетки, семантичные голые теги [внутри родительских селекторов и идентификаторов, без которых они бесполезны], например, при быстром прототипировании:

.menu,
#menu {
  ul {
    > li {
      > a {
        ...
      }
    }
  }
}

.selector,
#identifier {
  .grid {
    > div {
      ...
    }
  }
}

# Расширенные БЭМ-композиции

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

.block {
  &:hover,
  &::before,
  &__element,
  &--modifier {
    @include mobile {
      ...
    }
  }

  @include mobile {
    &:hover,
    &::before,
    &__element,
    &--modifier {
      ...
    }
  }
}

Чтобы всё стало окончательно прозрачно и понятно с этим, немного усложним пример с баннерами выше. Предположим, что в макетах страницы, для каждого блока-баннера нестандартным специфическим образом меняется типографика в зависимости от типоразмера экрана. Как лучше всего организовать разметку стилей и медиа-запросы в ней? Наверное никто не захочет делать это вот так — совершенно безобразно — очень много кода и медиа-запросов в нём:

.page-name {
  &__banner--1 {
    h2 {
      ...

      @include tablet {
          ...
      }

      @include mobile {
          ...
      }
    }

    p {
      ...
    }

    .btn {
      ...
    }
  }


  &__banner--2 {
    ...
  }
}

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

.page-name {
  &__banner--1 {
    ...
  }

  &__banner--2 {
    ...
  }

  @include tablet {
    &__banner--1 {
      ...
    }

    &__banner--2 {
      ...
    }
  }

  @include mobile {
    &__banner--1 {
      ...
    }

    &__banner--2 {
      ...
    }
  }
}

Для данного простого примера нужно организовать включение стилей для других типоразмеров вот так — каждый селектор баннера содержит всю информацию о нём в максимально понятной и удобной форме:

.page-name {
  &__banner--1 {
    ...

    @include tablet {
      ...
    }

    @include mobile {
      ...
    }
  }

  &__banner--2 {
    ...
  
    @include tablet {
      ...
    }
  
    @include mobile {
      ...
    }
  }
}

# Абстрактное связывание

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

// 1.
// Если это кейс "компонент в модификаторе внешней обертки" -
// в стилях компонента:
.component {
  .wrapper--modifier & {
    ...
  }
}

// 2.
// Если это кейс "переиспользуемый элемент или утилита в конкретном представлении" -
// в коде для этого вида:
#view-identifier {
  .utility,
  .component {
    ...
  }
}

Такой подход (как и первый кейс для прототипирования) действительно чреват следующими проблемами:

  • При злоупотреблении такой возможностью, бездумном неоправданном использовании растут риски встретится с конфликтами Специфичности;
  • В идеологическом плане, можно легко нарушить идеальную компонентность, добавив системе излишней связности.

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

Представим нам что-то нужно сделать с переиспользуемым компонентом .сomponent во всем интерфейсе только для iOS. У крупной обертки .layout будет через javascript выставляться модификатор .layout--iOS:

<main class="layout layout--iOS" role="main">

    <section>
    
        <div>
        
          <div>
          
            <div class="component"></div>
  
          </div>
        
        </div>   
    
    </section>

</main>

В этом случае стили нужно определить в компоненте, однозначно:

// В @/scr/scss/components/_component.scss:
.component {
  .layout--iOS & {
    ...
  }
}

Если же речь идет о разметке конкретного отдельного вида [а не «модификации на уровне всего интерфейса»], правильно будет использовать стили именно этого конкретного вида:

<main role="main">

    <section id="view">
        
        <div>
        
          <div>
          
            <div class="component"></div>
        
          </div>
        
        </div>   
    
    </section>

</main>
// В @/scr/scss/components/views/_view.scss:
#view {
  .component {
    ...
  }
}

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

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

  • локальные переменные объявляются (или глобальные переопределяются) перед любыми объявлениями и отделяются от деклараций новой строкой;
  • примешивания растворов идут после переменных перед всеми объявлениями свойств;
  • примешивания параметризированных микстур [даже если примесь не принимает параметров] идут после деклараций, исключение составляют случаи когда требуется локально для данного селектора перекрыть свойство объявленное в примеси;
  • сортировка правил осуществляется не по алфавиту, в случайном порядке, но, возможно, нестрого ориентируясь на концепцию Concentric-CSS;
  • вложенные селекторы и директивы, содержащие собственные объявления, всегда идут после новой строки (но если родительский тег не содержит собственных правил [и хочется по каким-то причинам сохранить уровень вложенности] пустую строку перед дочерним селектором можно опустить) в последовательности:
    1. псевдоклассы;
    2. псевдоэлементы;
    3. вложенные дочерние селекторы;
    4. вложенные дочерние селекторы-модификаторы;
    5. вложенные утилиты;
    6. примеси с @content и медиа-запросы (медиа-запросы в стандартном случае имеют вид такой примеси);
    7. родительские, по отношению к данному, селекторы перед Parent (&).
// Глобальные переменные в @/src/scss/utils/_variables.scss:
$variable--1: value;


// Сборник миксинов проекта в @/src/scss/utils/_mixins.scss:
@mixin mixin-name($variable1, $variable2) {
  property--1: $variable1;
  property--2: $variable2;
}


// Сборник растворов проекта в @/src/scss/utils/_placeholders.scss:
%placeholder-name {
  property--3: value;
  property--4: $variable--3;
}


// В любом месте проекта:
.selector {
  // Переменные
  $variable--2: value;
  $variable--3: value;

  // Растворы
  @extend %placeholder-name;
  // Правила
  property--5: value;
  property--6: value;
  // Примеси
  @include mixin-name($variable--2, $variable--3);
  property--2: value; // исключение - переписываем свойство в примеси

  // Псевдоклассы
  &:hover {
    ...

    a {color: $variable-03;} // допустимо на одной строке, если одно правило
  }

  // Псевдоэлементы
  &::after {
    ...
  }

  // Дочерние селекторы
  &__element {
    ...
  }

  // Модификаторы
  &--modifier {
    ...
  }

  // Утилиты
  .utility {
    ...
  }

  // Примеси с @content
  @include mixin-02 {
    @content;
  }

  // Родительские обертки перед &
  .wrapper & {
    ...
  }
}

# Комментирование

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

  • для комментирования используется только однострочный тип комментария начинающийся с двух слешей (//);
  • комментарий к селектору идёт на строчке непосредственно перед ним, комментарий к группе селекторов отбивается пустой строкой;
  • комментарий к селектору, группе селекторов, разделу кода располагается на отдельной строке, комментарий к свойству - на той же строке что и комментируемое объявление через пробел после него;
  • комментарий к разделу отбивается двумя пустыми строками сверху, одной строкой или двумя (заголовок файла или очень большого-важного фрагмента) строками с большим количеством слешей и пустой строкой снизу.
// Это селектор
.selector {
  property: value; // комментарий к свойству
  ...
}

// А это селекторы

.selector1 {
  ...
}

.selector2 {
  ...
}


// Sizes
//////////////////////////////////////////////////////

// Main
$gutter: 32px;


// Раствор для усечения и добавления многоточия
// в слишком длинную строку на одной строке
// 1. Предотвращает сворачивание содержимого, оставляет его на одной строке.
// 2. Добавляет многоточие на конце строки.
%string-overflow-protection {
  white-space: nowrap; // 1
  text-overflow: ellipsis; // 2
  overflow: hidden;
}


// Примесь для выставления размера блоку
// @author Левон Гамбарян
//
// @param {Length} $width - ширина элемента
// @param {Length} $height - высота элемента
//
// @example usage:
// .selector {
//   @include size(100%, 200px);
// }
//
// @example output:
// .selector {
//   width: 100%;
//   height: 200px;
// }
@mixin size($width, $height) {
  width: $width;
  height: $height;
}

# Компонентность

Мы выделяем некую сущность в отдельный компонент, если:

  • она описывает, обслуживает какую-то отдельную определенную функциональность;
  • может быть использована повторно и/или модифицирована;
  • ее отношения с другими компонентами чётко определены.

При использовании компонентных фреймворков с препроцессорами крайне соблазнительной и сильной кажется идея предоставить стилевую базу компонентам. Таким образом, мы решаем сразу все проблемы - получая и мощную глобальную абстракцию препроцессора, и лаконичную оптимальную инкапсуляцию частных стилей в компонентах. Пока я заметил только один незначительный минус этого подхода - в dev-режиме во вкладке инспектора которая отображает свойства применённые к текущему выбранному элементу - возникает сильное наслоение дублирующихся свойств и пользоваться ей становится неудобно. С другой стороны, вы должны писать так идеально, чтобы у вас вообще не возникало необходимости в отладке.)

# Vue

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

<template>
  <!-- ... Разметка компонента -->
</template>

<script>
  export default {
    // ... Логика компонента
  };
</script>

<style lang="scss" scoped> /* scoped-стили - только для этого компонента */
  @import "@/scss/_stylebase.scss"; /* вот эта строчка решает все проблемы */

  /* Локальные переменные компонента (и миксины если нужно) */
  $component__variable--1: value;
  $component__variable--2: value;

  @mixin component__mixin--1($variable) {
    /* ... */
  }

  .component {
    /* Растворы, правила, примеси */
    @extend %placeholder;
    property-01: value;
    property-02: value;
    @include component__mixin--1($component__$variable--1);
    @include global-mixin--1($component__variable--2, $global-variable--1);

    /* Дочерние селекторы */
    &__element {
      /* ... */
    }

    /* Модификаторы */
    &--modifier {
      /* ... */
    }

    /* Примеси с @content */
    @include global-mixin--2() {
      /* ... */
    }
  }
</style>

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

Давайте на примере изучим простейший конфликт специфичности при использовании перенормализованной голой семантики со scoped на компонентах. Вот в этом моем стартовом проекте, который мы еще будем ковырять в самом начале второй части пособия при разборе кейса с темизацией: Vue c SCSS.

У нас есть следующие компоненты: Layuot - основная GUI-обертка, которая использует обертку Header со слотом в который передаются компоненты-переключатели. В данном случае, мы пишем быстрый прототип, стартовый шаблон, избегая излишней детализации. Для оберток мы выставляем scoped в стилях компонента, а переключатели несут разный функционал, очевидно являясь разными компонентами, но, при этом, скорее всего, могут выглядеть одинаково, и, поэтому, несут один и тот же класс для стилизации. Очень важно вот это понять: мы разделяем оформление-кастомизацию с помощью препроцессора и компонентную функциональную логику интерфейса. Кроме того, так как это быстрый прототип, для разметки переключателя мы логично-семантично решаем использовать теги списка.

<!--  В @/scr/components/Layout/Layout.vue: -->
<template>
  <div class="layout">
    <Header>
      <LangSwitch />
      <ThemeSwitch />
    </Header>
    <slot />
  </div>
</template>

<script>
  import Header from '@/components/Layout/Header.vue';
  import LangSwitch from '@/components/Elements/LangSwitch.vue';
  import ThemeSwitch from '@/components/Elements/ThemeSwitch.vue';
  
  export default {
    name: 'Layout',
  
    components: {
      Header,
      LangSwitch,
      ThemeSwitch,
    },
  };
</script>

<style lang="scss" scoped>
  @import "@/styles/_stylebase.scss";

  .layout {
    /* ... стили компонента */
  }
</style>
<!--  В @/scr/components/Layout/Header.vue: -->
<template>
  <div class="header">
    <slot />
  </div>
</template>

<script>
  export default {
    name: 'Header',
  };
</script>

<style lang="scss" scoped>
  @import "@/styles/_stylebase.scss";

  .header {
    /* ... стили компонента */
  }
</style>

Один из переключателей - для смены темы оформления - без стилей вообще:

<!--  В @/scr/components/Elemens/ThemeSwitch.vue: -->
<template>
  <ul class="switch">
    <li
      v-for="value in themes"
      v-bind:key="value"
    >
      <a v-if="value !== theme"
        href="#"
        @click.prevent="changeTheme(value)"
      >{{ value }}</a>
      <span v-else>{{ value }}</span>
    </li>
  </ul>
</template>

<script>
  // ... логика переключателя
</script>

Для оформления обоих переключателей используется класс в файле препроцессора с описанием виджетов:

// В @/scr/scss/project/_widgets.scss:
.switch {
  // ...
  // следующая строчка не заработает без флага !important !!!
  padding: ($gutter / 4) ($gutter / 2) !important; // перекрываем base в хэшах

  > li {
    // ...

    a {
      // ...
    }

    // слэш между вариантами
    &:not(:last-child) {
      &:after {
        margin-left: $gutter / 4;
        margin-right: $gutter / 4;
        text-align: center;
        content: " / ";
      }
    }
  }

  // отступ между переключателями
  & + .switch {
    margin-left: $gutter;
  }
}

Теперь давайте заглянем в инспектор и найдем переключатель:

Прежде всего, мы обнаруживаем что хэш стилей от Header - встает правее чем собственный класс компонента переключателя. Но главная суть проблемы и конфликта не в этом. Дело в том, что мы, вероятно, слишком увлеклись нормализацией в файле @/src/scss/_base.scss стилевой базы:

// @/scr/scss/core/_base.scss
// Lists
ul {
  list-style-type: none;
  padding: 0; // вот это становится проблемой
  margin: 0;
}

Таким образом, даже если мы отменим хэширование стилей для Header, все равно специфичность голого тага-элемента с атрибутом ul[data-v-xxxxxxxx] из левого хэша от Layout окажется выше чем у простого селектора по классу. Также, можно наблюдать «наслоение правил от стилевой базы» которое дает компонентность.

В этой ситуации мы можем:

  • Самый адекватный вариант: убрать обнуление внутренних отступов у списка из нормализации, если мы собираемся использовать голую семантику - слишком навязчиво будет что-то делать с ее оформлением в стилевой базе;
  • Самый раздолбайский вариант: согласиться на !important как показано выше и не париться - жизнь и так слишком коротка чтобы переживать из-за незначительных мелочей!)));
  • Понятно: не использовать семантику списка - сверстать все на div и классах;
  • Не использовать scoped на компонентах-обертках. Тогда стили не будут «наслаиваться через хэши» и специфичность селектора по классу окажется выше чем специфичность «голого» элемента ul нормализации в базе.

# React

С React все более прозаично - у меня есть папка @/src/scss/components/ куда я складирую стили отдельных компонентов, которые, если это необходимо - задействуют медиатор, стилевую базу.

.
└─ src
   └─ sscs
      ├─ components
      │  ├─ utils
      │  │  ├─ _loader.scss
      │  │  └─ ...
      │  └─ ...
      └─ ...

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

Вот - самый простой компонент - Loader, для примера, стили:

// В @/src/scss/component/utils/_loader.scss
// Loader
//////////////////////////////////////////////////////
//////////////////////////////////////////////////////

// подтягиваем стилевую базу, если нужно, здесь - ради основного цвета
@import "../../_stylebase.scss";

.loader {
  $loader_size_default: 100px;
 
  @include size($loader_size_default, $loader_size_default);

  svg path {
    fill: $color_primary;
  }
}

Сам компонент, в @/src/components/utils/Loader.jsx:

// В @/src/components/utils/Loader.jsx
import React from "react";
import PropTypes from "prop-types";

import '../../scss/components/utils/_loader.scss'; // подтягиваем стили компонента

const Loader = ({size}) => {
  return (
    <div className="loader">
      <svg
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
        x="0px"
        y="0px"
        width={size}
        height={size}
        viewBox={`0 0 ${size / 2} ${size / 2}`}>
        <path
          fill="#000"
          d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z">
          <animateTransform
            attributeType="xml"
            attributeName="transform"
            type="rotate"
            from="0 25 25"
            to="360 25 25"
            dur="0.4s"
            repeatCount="indefinite"/>
        </path>
      </svg>
    </div>
  );
};

Loader.defaultProps = {
  size: 100,
};

Loader.propTypes = {
  size: PropTypes.number,
};

export default Loader;

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

# Styled Components

Для полноты повествования, в теме про компонентность нельзя не вернуться к альтернативному направлению развития CSS-технологий, упомянутому в самом начале. Возможно ли реализовать некоторые из рассмотренных выше преимуществ глобального препроцессора используя CSS-in-JS подход, например, Styled Components? В некотором ограниченном виде - да, возможно.

Мы легко можем использовать javascript для того чтобы определить объект с глобальными константами на которые в дальнейшем и будем опираться в единообразном и управляемом оформлении компонент. То есть, сформировать гайдлайн, и, например, стандартизировать в нем цвета и брекпоинты.

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

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

Кроме того, понятно что, во всех остальных аспектах наше оформление останется плоским и изолированным по компонентам, глухим и несвязным. Любая переиспользуемость будет ограничена компонентностью и заключатся только лишь в ней. Кто-то в какой-то ситуации может быть заинтересован как раз именно в такой строгости и скуке. Может быть вы неспешно пилите продукт с минималистичными простыми видами, скудным оформлением интерфейса... Но в самом общем случае, для быстрой работы со сложными макетами этот подход пока что представляется очень сомнительным и неудачным, ограниченным.

# Библиотека

Совмещение компонентного фреймворка с мощным глобальным препроцессором и, очень желательно - минимально подробной документацией, закономерно приводит нас к мысли о библиотеке. Компонентная библиотека с препроцессором очевидно способна служить ядром для целой экосистемы проектов использующей один единый стиль в оформлении и одинаковые UI-компоненты, шаблоны. И поверьте, это действительно реальный жизненный кейс. Бренд обычно использует один фирменный стиль, но вполне может иметь несколько проектов базирующихся на нем. С этим вы действительно можете легко столкнуться в обычной практике.

Именно для целей организации библиотеки, популярный фреймворк Vue выглядит более пригодным и удобным чем самый популярный React, за счет следующих тонкостей:

  • Наглядные однофайловые компоненты: с изолируемыми только для компонента scoped-стилями на любом препроцессоре прямо в коде такого однофайлового компонента;
  • Очень важно для целей библиотеки - проработанная развитая система дистрибуции контента - Слоты;
  • При необходимости, тоже очень удобно - готовые переходы и анимации для компонентов - на уровне JSX;
  • Наличие удобных инструментов для документирования: смотрите VuePress на котором написано это руководство.

Еще одна специфическая и странная интересная возможность прогрессивного фреймворка Vue, будет упомянута во второй части этого пособия, когда речь зайдет о способах взаимодействия между компонентами.

# Готовые решения

В этом «мощном вводе», знакомством с практически эффективными методами использования препроцессора, уже затронуто несколько достаточно спорных холиварных тем, которые, вполне вероятно способны вызвать бурление у некоторых специалистов: препроцессоры против CSS-in-JS для компонентности, использование голой семантики или абстрактного связывания при композиции селекторов... И в заключение темы, «до кучи», просто необходимо навести ясность еще по одной общей проблеме, игнорирование которой может принести массу затруднений и стать серьезной помехой для гармоничного и эффективного развития проекта GUI веб-интерфейса.

Если совсем кратко и четко: почти никогда не опирайтесь на объемные готовые решения, крупные библиотеки для реализации и кастомизации GUI.

Я не думаю что прочитав этот спорный совет, вы мгновенно примете его на веру и, откинув свои привычки, послушно побежите вычищать «всякие bootstrap`ы» из своих проектов. Нет - айтишники не такие, конечно же... Думаю, что еще много раз большинство будет наступать на грабли, корячиться с чудовищными кряками и так далее... Готовые библиотеки обещают «рай земной», когда практически все уже сделано до вас и за вас, а вам якобы просто остается это грамотно применить. Детализация и проработанность, универсальность некоторых решений, на первый взгляд, действительно поражает воображение и выглядит крайне соблазнительно. Но в реальной жизни, как всегда, все совсем не так как на стартовом лендинге. И, на самом деле - вы просто боитесь. Боитесь, что не сможете реализовать простейшие элементы и поведение быстро и качественно в требуемый срок. Боитесь того, что не понимаете как организовать оформление интерфейса по-настоящему глубоко и грамотно. И тут на помощь приходит готовая библиотека - «кубик-рубика», а вы из дизайнера и программиста превращаетесь в менеджера чужих решений «по скрипту».)

Стоит подробнее остановиться именно на психологическом и воспитательном аспектах проблемы. Выбирая чужую библиотеку для GUI - вы потихоньку отвыкаете писать свой, лаконичный и надежный код, часто применяя вместо простых, прозрачных и самостоятельных крафтовых решений, ради которых необходимо совсем чуть-чуть «подумать и потрудиться», по сути - «кота в мешке». По моему убеждению, любая такая «универсальная библиотека» это настоящая «мышеловка с сыром на блюде» для нерадивых и закомплексованных разработчиков. И чем более детализирована и универсальна, объемна библиотека - тем, на самом деле, только хуже для вас. Использование таких решений, имхо, оправдано только когда необходимо быстро создать подробные прототипы с действительно сложными UI/UX и виджетами, но без какого-то кастомного стиля, при этом.

Вам вообще не нужен Bootsrap, например, чтобы быстро писать сетки для modern. Любая сетка, пространственная раскладка и композиция сегодня эффективно реализуются на Grid и Flexbox даже при самом минимальном знакомстве с этими технологиями. И решение, вероятно, будет намного более точным и адекватным конкретной ситуации. Но заслуженный и по-прежнему популярный дедушка таких решений Bootsrap, это, имхо, еще не самое страшное зло. Bootsrap достаточно ненавязчиво предоставляет легкую систему шаблонов-прототипов, почти не стилизованной разметки, удобно высовывая наружу переменные, для быстрой кастомизации. И он при этом хотя бы не решается агрессивно диктовать вам практически целую дизайн-систему, сложные лейауты или конкретные UI-решения, иногда даже чисто оформительского качества, как, например, видимо, считают возможным поступать более молодые «GUI-библиотеки для компонентных фреймворков» Vuetify или Ant Design. Все случаи и меняющиеся желания заказчиков предусмотреть невозможно. Вы легко можете сесть в глубокую лужу с неадекватным поведением или оформлением готовых элементов. Возможности свободной кастомизации жестко органичены, кастомизация предоставлена практически только через готовые методы API компонентов. Кроме того верстка на Vuetify или Quasar, например, приводит к тому, что ваши шаблоны начинают выглядеть как уже совсем не ваши), ну не круто, явно не так как я привык и хочу их видеть. В общем смысле, принимая решение об использовании в проекте такого якобы «на все готового» монстра, вы обрекаете при этом себя принять многие его правила игры, синтаксис, подходы... Когда это может быть оправдано? Ради модали и тултипа, вы вводите в систему еще один жесткий и практически непрозрачный слой, диктующий свои подходы?

Я заметил что на практике при задействовании в проектах большой готовой библиотеки для GUI и шаблонов, часто - непростительно много времени уходит именно на «борьбуработу с инструментом», вместо того чтобы осуществлялся прогресс конкретно по проекту. И при этом, такое, якобы общее универсальное, рамочное решение, по факту, в реальности очень часто все равно не приводит к надежному закрытию всех частных проблем и запросов. Если кратко:

  • Библиотеки не предоставляют ничего, чего, на самом деле, нельзя или быстро и лаконично написать на препроцессоре с компонентным фреймворком самому, или, в сложных, неподъёмно дорогих случаях - найти или даже найти, форкнуть и допилить по уму под нужные требования частную точечную, более толерантную и органичную минимальную реализацию - модуль;
  • Тоталитарно относятся к вашему стилю кода, стандартам и подходам, агрессивно навязывают свои правила, изначально очевидно излишни и громоздки;
  • Затрудняют, а не облегчают нанесение специфической кастомизации и оформления, создание сложных лейаутов и поведения;
  • Не оправдывают затрат и очень часто все равно не решают всех проблем, не способны удовлетворять часто меняющимся требованиям.

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