CSS-препроцессоры

Взлёт, расцвет и закат эпохи препроцессоров. И как это связано с развитием нативного CSS.

Время чтения: больше 15 мин

Кратко

Скопировано

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

Основные препроцессоры — это Sass, Less, SCSS и Stylus. Они отличаются синтаксисом.

С чего начать?

Скопировано

Установка препроцессоров происходит, как правило, через npm или pnpm. Потом вы дописываете конфигурационные файлы (конфиги) по инструкции к нужному препроцессору и настраиваете под свой проект.

Хорошая поддержка препроцессоров у сборщика Vite. У него есть встроенная поддержка файлов .scss, .sass, .less, .styl и .stylus. Для этого не нужно устанавливать специфичные для Vite плагины, но сам препроцессор должен быть установлен через команду npm -add -D <название препроцессора>. Установка препроцессоров в связке с Webpack сложнее, лучше обратиться к документации сборщика.

Зачем знать о препроцессорах?

Скопировано

Сейчас можно обойтись без препроцессоров, так как у CSS больше возможностей, чем раньше 😇 Хотя препроцессоры из-за этого теряют популярность, вы всё равно найдёте проекты, в которых не используют чистый CSS. Так что, при очередной смене работы, можете столкнуться с проектом, написанным на Less или SCSS.

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

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

Немного истории

Скопировано

Препроцессоры появились по нескольким причинам. Раньше CSS не был таким развитым и гибким, как сейчас. В нём не было переменных, операторов, циклов, условий, нельзя было писать вложенные конструкции. Одновременно была проблема в довольно большом расхождении между разными браузерами, особенно между Safari и Internet Explorer. Для того чтобы сгладить разницу в отображении интерфейсов сайтов в браузерах, использовали препроцессорные миксины. Примерно в начале 2010-х годов появилось много новых устройств, помимо компьютеров и ноутбуков, и сайты на них тоже должны были выглядеть хорошо. Экраны также отличались друг от друга по техническим характеристикам, например, по размерам и особенностям передачи цветов, так что появилась необходимость работать со сложными цветами. Все эти проблемы решали препроцессоры, а за счёт повторного использования переменных и специфического синтаксиса, код был изящнее и проще для чтения.

Вы можете спросить, а почему вообще важно количество строк в файлах для стилей? Раньше, где-то между 2013 и 2019, React-компоненты писали на громоздких классах с большим количеством свойств. Это приводило к длинному полотну из CSS-классов и правил, которое хотелось сократить для удобства чтения и для лучшей поддержки. Постепенно появилась тенденция к декомпозиции (разбиению) компонентов на более мелкие, чтобы улучшить их переиспользуемость и ускорить навигацию по стилям. Это вызвало запрос на минималистичный CSS. Сейчас декомпозиция считается лучшей практикой, но большие запутанные компоненты всё ещё встречаются в легаси-коде.

С тех пор многое изменилось. В CSS появились кастомные свойства, вложенность и другие нововведения. Также браузерные команды начали совместно работать над исправлением проблем совместимости браузеров (Interop), а Internet Explorer вообще перестал поддерживаться.

Синтаксис и дополнительные возможности

Скопировано

Less и SCSS похожи синтаксисом на современный CSS, различий немного. К примеру, если не использовать дополнительные фичи SCSS, трудно найти отличия между ним и CSS:

        
          
          @media screen and (max-width: 600px) {  .error {    width: 100%;  }}.error {  width: 300px;}
          @media screen and (max-width: 600px) {
  .error {
    width: 100%;
  }
}

.error {
  width: 300px;
}

        
        
          
        
      

Этот же код, но на Less:

        
          
          @media {  screen {    and {      (max-width {        &:600px) {          .error {            width: 100%;        }      }    }  }}.error {  width: 300px;}
          @media {
  screen {
    and {
      (max-width {
        &:600px) {
          .error {
            width: 100%;
        }
      }
    }
  }
}

.error {
  width: 300px;
}

        
        
          
        
      

Синтаксис Sass и Stylus напоминает синтаксис языка программирования Python. В них тоже можно опускать фигурные скобки и знаки : (двоеточие) и ; (точка с запятой), поэтому важны правильные отступы. Если отступы в Stylus и Sass расставлены неправильно, препроцессорные стили не скомпилируются в CSS, а вы получите консольную ошибку.

Простой пример синтаксиса Sass:

        
          
          @media screen and (max-width: 600px)  .error    width: 100%.error  width: 300px
          @media screen and (max-width: 600px)
  .error
    width: 100%

.error
  width: 300px

        
        
          
        
      

Больше подробностей о тонкостях синтаксиса препроцессоров найдёте в официальной документации или в шпаргалках, например, Stylus cheatsheet. Можете также почитать обзорную статью про разные препроцессоры и их фичи, которых довольно много.

Дополнительные возможности препроцессоров часто называют синтаксическим «сахаром» (syntactic sugar). Они не изменяют существующие возможности CSS и нужны для повышения читаемости кода и ускорения его написания.

@extend

Скопировано

Во всех препроцессорах есть интересная фича — @extend. С её помощью расширяют классы за счёт других. С одной стороны, это удобно и сокращает количество написанного кода, с другой, если неправильно использовать фичу, это приведёт к хаосу в организации кода. Станет трудно разобраться, какие CSS-стили получатся в итоге. Дело в том, что из-за @extend возникает неявная связанность между оригинальными классами, и теми классами, которые их расширяют. Изменяя стили из первоначального класса, мы автоматически вносим изменения и во все другие, которые содержат расширения. Так что, при большом количестве расширений, стили могут измениться сразу в нескольких местах. Важно следить за тем, есть ли у родительских стилей зависимость от дочерних. Таким образом, при использовании @extend стоит сначала подумать, будет ли это хорошим решением для конкретного проекта, и действительно ли оно упрощает вашу работу.

Давайте посмотрим на неудачный пример использования @extend. Предположим, у нас есть класс .button со свойствами для ширины, высоты, цвета фона и какой-то анимации. Этот класс расширяет другие в разных местах, создавая связь между ними. Теперь, внося изменения в первоначальный класс .button, нужно следить за стилями у других классов, которые расширяются за его счёт. А что, если один класс расширяют несколько, и в них повторяются одни и те же свойства и их значения? Или расширений слишком много? В таких случаях сложно понять, какие стили применятся к элементам интерфейса в итоге, и в такой код трудно вносить новые изменения.

С другой стороны, если @extend содержит не так много стилей и расширения не навешиваются хаотично на один и тот же класс, то это не запутывает связи между ними. Это хороший пример использования фичи.

Рассмотрим @extend на примере Stylus. Расширим стили класса .warning за счёт стилей .message:

        
          
          .message  padding: 10px  border: 1px solid #EEEEEE.warning  @extend .message  color: #E2E21E
          .message
  padding: 10px
  border: 1px solid #EEEEEE

.warning
  @extend .message
  color: #E2E21E

        
        
          
        
      

Этот код скомпилируется в такой CSS:

        
          
          .message,.warning {  padding: 10px;  border: 1px solid #EEEEEE;}.warning {  color: #E2E21E;}
          .message,
.warning {
  padding: 10px;
  border: 1px solid #EEEEEE;
}

.warning {
  color: #E2E21E;
}

        
        
          
        
      

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

А вот так @extend выглядит в Sass:

        
          
          .error  border: 2px #FFA500  background-color: #DDDDDD  &--serious    @extend .error    border-width: 5px
          .error
  border: 2px #FFA500
  background-color: #DDDDDD

  &--serious
    @extend .error
    border-width: 5px

        
        
          
        
      

Миксины

Скопировано

Миксины (mixins) похожи на функции в языках программирования. В них передают аргументы, задают им значения по умолчанию и так далее. Миксины помогают группировать нужные стили и повторно использовать их в нескольких местах кода или в разных CSS-файлах. Это пригодится, когда в проекте ну очень много стилей и строк кода, сложная логика вычисления значений для CSS-свойств, а ещё когда хотите подстраховаться и не споткнуться об разные особенности отрисовки стилей в браузерах.

Попробуем при помощи SCSS-миксина сократить время на написание медиавыражений для минимальной ширины экрана, которую поддерживаем на сайте. Вот нужные нам брейкпоинты (контрольные точки):

        
          
          $breakpoints: (  xs: 0,  sm: 576px,  md: 768px,  lg: 992px,  xl: 1200px,  xxl: 1400px);
          $breakpoints: (
  xs: 0,
  sm: 576px,
  md: 768px,
  lg: 992px,
  xl: 1200px,
  xxl: 1400px
);

        
        
          
        
      

Теперь напишем миксин для @media:

        
          
          @mixin media($widthName) {  $width: map-get($breakpoints, $widthName);  @media (width >= $width) {    @content;  }}
          @mixin media($widthName) {
  $width: map-get($breakpoints, $widthName);
  @media (width >= $width) {
    @content;
  }
}

        
        
          
        
      

В нужном месте используем его с помощью специальной директивы @include:

        
          
          .card {  padding: 8px;  @include media(md) {    padding: 16px;  }}
          .card {
  padding: 8px;

  @include media(md) {
    padding: 16px;
  }
}

        
        
          
        
      

Скомпилированный CSS выглядит таким образом:

        
          
          .card {  padding: 8px;}@media(width >= 768px) {  .card {    padding: 16px;  }}
          .card {
  padding: 8px;
}

@media(width >= 768px) {
  .card {
    padding: 16px;
  }
}

        
        
          
        
      

Давайте теперь напишем миксин для класса со стилями фокуса в Less. Предположим, во всех случаях нам подходит чёрный цвет (#000000) обводки. В зависимости от размеров и формы элементов, обводка бывает разной толщины (outline-width) и находится на разном расстоянии от краёв элемента (outline-offset).

        
          
          .focus(@width; @offset; @color: #000000) {  outline-width: @width;  outline-offset: @offset;  outline-color: @color;}
          .focus(@width; @offset; @color: #000000) {
  outline-width: @width;
  outline-offset: @offset;
  outline-color: @color;
}

        
        
          
        
      

Применим миксин к кнопкам и ссылкам и зададим нужные стили обводке:

        
          
          .button:focus-visible {  .focus(2px; 2px);}.link:focus-visible {  .focus(3px; 3px);}
          .button:focus-visible {
  .focus(2px; 2px);
}

.link:focus-visible {
  .focus(3px; 3px);
}

        
        
          
        
      

Так будет выглядеть получившийся CSS:

        
          
          .button:focus-visible {  outline-width: 2px;  outline-offset: 2px;  outline-color: #000000;}.link:focus-visible {  outline-width: 3px;  outline-offset: 3px;  outline-color: #000000;}
          .button:focus-visible {
  outline-width: 2px;
  outline-offset: 2px;
  outline-color: #000000;
}

.link:focus-visible {
  outline-width: 3px;
  outline-offset: 3px;
  outline-color: #000000;
}

        
        
          
        
      

Возможности Stylus

Скопировано

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

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

        
          
          .button  width 200px  height @width
          .button
  width 200px
  height @width

        
        
          
        
      

Получится такой CSS:

        
          
          .button {  width: 200px;  height: 200px;}
          .button {
  width: 200px;
  height: 200px;
}

        
        
          
        
      

Другой способ сокращённой записи — шорткат @block. С его помощью блок кода определяют в переменной, которую можно вызвать, передать как аргумент или повторно использовать другим способом. Есть несколько способов написания:

        
          
          foo =  width: 20px  height: 20pxfoo = @block {  width: 20px  height: 20px}
          foo =
  width: 20px
  height: 20px

foo = @block {
  width: 20px
  height: 20px
}

        
        
          
        
      

И, если нужно отрисовать блок foo, можно вызвать переменную внутри интерполяции:

        
          
          .icon  {foo}
          .icon
  {foo}

        
        
          
        
      

Код из предыдущего примера скомпилируется в такой CSS:

        
          
          .icon {  width: 20px;  height: 20px;}
          .icon {
  width: 20px;
  height: 20px;
}

        
        
          
        
      

Получившийся CSS-код можно проверить в конвертере Stylus to CSS.

Возможности SCSS

Скопировано

В SCSS, как и в других препроцессорах, можно вкладывать одни селекторы в другие. Это называют вложенностью стилей (nesting).

        
          
          .button {  color: #000000;  background-color: #FFFFFF;  .button-icon {    width: 10px;    height: 10px;  }}
          .button {
  color: #000000;
  background-color: #FFFFFF;

  .button-icon {
    width: 10px;
    height: 10px;
  }
}

        
        
          
        
      

Скомпилированный CSS:

        
          
          .button {  color: #000000;  background-color: #FFFFFF;}.button .button-icon {  width: 10px;  height: 10px;}
          .button {
  color: #000000;
  background-color: #FFFFFF;
}

.button .button-icon {
  width: 10px;
  height: 10px;
}

        
        
          
        
      

Также можно использовать оператор &, чтобы связать родительский селектор с псевдоклассом. К примеру, у нас есть кнопка и стили при наведении на неё:

        
          
          .button {  color: #FFFFFF;  background-color: #000000;  &:hover {    color: #000000;    background-color: #FFFFFF;  }}
          .button {
  color: #FFFFFF;
  background-color: #000000;

  &:hover {
    color: #000000;
    background-color: #FFFFFF;
  }
}

        
        
          
        
      

На выходе получится такой CSS-код:

        
          
          .button {  color: #FFFFFF;  background-color: #000000;}.button:hover {  color: #000000;  background-color: #FFFFFF;}
          .button {
  color: #FFFFFF;
  background-color: #000000;
}

.button:hover {
  color: #000000;
  background-color: #FFFFFF;
}

        
        
          
        
      

Оператор & удобно использовать при именовании классов по методологии БЭМ (Блок, Элемент, Модификатор). В этом примере объединяем блок .button с классом элемента .button__icon и с классом модификатора .button_inactive:

        
          
          .button {  color: #FFFFFF;  background-color: #000000;  &_inactive {    opacity: 0.7;  }  &__icon {    width: 20px;    height: 20px;  }}
          .button {
  color: #FFFFFF;
  background-color: #000000;

  &_inactive {
    opacity: 0.7;
  }

  &__icon {
    width: 20px;
    height: 20px;
  }
}

        
        
          
        
      

Этот код скомпилируется в CSS таким образом:

        
          
          .button {  color: #FFFFFF;  background-color: #000000;}.button_inactive {  opacity: 0.7;}.button__icon {  width: 20px;  height: 20px;}
          .button {
  color: #FFFFFF;
  background-color: #000000;
}

.button_inactive {
  opacity: 0.7;
}

.button__icon {
  width: 20px;
  height: 20px;
}

        
        
          
        
      

Когда используете вложенность стилей в SCSS, не злоупотребляйте этой возможностью. Если вложить друг в друга много классов, такой код будет трудно читать и обновлять (особенно после отпуска). Обычно рекомендуют не создавать больше двух уровней вложенности.

Посмотрим на плохой пример и попробуем мысленно скомпилировать CSS 😅

        
          
          .navigation {  .link {    padding: 10px 20px;    &:hover {      background: #000000;      color: #FFFFFF;    }    &__icon {      width: 20px 20px;      margin-right: 5px;      &_big {        width: 60px 60px;        &_secondary {          color: #DDDDDD;        }      }    }  }}
          .navigation {
  .link {
    padding: 10px 20px;

    &:hover {
      background: #000000;
      color: #FFFFFF;
    }

    &__icon {
      width: 20px 20px;
      margin-right: 5px;

      &_big {
        width: 60px 60px;

        &_secondary {
          color: #DDDDDD;
        }
      }
    }
  }
}

        
        
          
        
      

Теперь сверьте свой мысленный результат компиляции с реальным:

        
          
          .navigation .link {  padding: 10px 20px;}.navigation .link:hover {  background: #000000;  color: #FFFFFF;}.navigation .link__icon {  width: 20px 20px;  margin-right: 5px;}.navigation .link__icon_big {  width: 60px 60px;}.navigation .link__icon_big_secondary {  color: #DDDDDD;}
          .navigation .link {
  padding: 10px 20px;
}

.navigation .link:hover {
  background: #000000;
  color: #FFFFFF;
}

.navigation .link__icon {
  width: 20px 20px;
  margin-right: 5px;
}

.navigation .link__icon_big {
  width: 60px 60px;
}

.navigation .link__icon_big_secondary {
  color: #DDDDDD;
}

        
        
          
        
      

Поиграть с разными возможностями SCSS и похожим на него Sass можно в Sass Playground.

Возможности Less

Скопировано

Попробуем поработать с препроцессором Less. Для класса .block добавляем к части от ранее заданной ширины дополнительную величину. Примерно для того же сейчас используется CSS-функция calc(). Для другого класса .block1 задаём переменным значения, а потом, с помощью математических вычислений, умножаем их между собой и делим на 3. Это тоже работает как calc().

        
          
          .block {  width: 10% + 30px;}@w1 = 10%;@w2 = 30px;.block1 {  width: (@w1 * @w2) / 3;}
          .block {
  width: 10% + 30px;
}

@w1 = 10%;
@w2 = 30px;

.block1 {
  width: (@w1 * @w2) / 3;
}

        
        
          
        
      

Благодаря возможностям Less и других препроцессоров можно задавать значения переменным и оперировать ими как математическими.

Мы уже упоминали про переменные в препроцессорах. С помощью них удобно хранить повторяющиеся значения свойств в одном месте. В Less для переменных используют знак @ (коммерческое at). При этом можно ссылаться в одних переменных на другие. В этом примере храним в переменных значения для свойств width и height. Внутри переменной @height переиспользуем переменную @width и прибавляем к её значению дополнительные 10 пикселей:

        
          
          @width: 100px;@height: @width + 10px;.card {  width: @width;  height: @height;}
          @width: 100px;
@height: @width + 10px;

.card {
  width: @width;
  height: @height;
}

        
        
          
        
      

На практике

Скопировано

Разобраться в препроцессорах поможет официальная документация. Ещё есть много конвертеров, которые конвертируют код из одних препроцессоров в другие, в CSS и наоборот.

Примеры конвертеров:

Можно найти конвертеры и в виде npm- и pnpm-пакетов, например, npm-конвертер для Stylus. Однако у пакетов может быть немного скачиваний, и их сложнее использовать, чем просто онлайн-конвертер. Сложность в основном в том, что любой новый пакет создаёт свои зависимости, включая препроцессоры. Чем больше зависимостей, тем больше вероятность, что появятся сложности в версионировании.

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