Какими CSS обладает особенностями, которые приносят боль на больших проектах?

  • глобальное пространство имен
  • разрешение зависимостей
  • поиск «мертвого» кода
  • отсутствие констант
  • неоднозначный результат (каскад)

Давайте разберемся с тем, как мы сейчас пишем CSS на больших проекта и как хотелось бы его писать в идеальном мире.

Возьмем простой пример: кнопка и ее состояния.

В реальности


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

В БЭМ-нотации это бы выглядело так:

.button {...}
.button_state_disabled {...}
.button_state_error {...}
.button_state_progress {...}


Я думаю, что многие согласятся с тем, что при первом знакомстве с БЭМ когнитивный диссонанс вызывают огромные названия классов, которые получаются в итоге.

Естественно было бы написать:

.button {...}
.button.disabled {...}


Однако, через месяц, когда все забудут про это место, появится класс .disabled в другом файле(который будет означать совсем другое), а здесь неожиданно сломается — пространство имен единое.

Можно было бы написать так:

.button {...}
.button-disabled {...}


Но тогда получается слишком много дублирования кода, потому что кнопки отличаются всего одним стилем: .button-disabled должен содержать все то же, что и .button, но, например, другой цвет фона.

Сейча эта задача решается с использованием миксинов на уровне препроцессоров, потому что в CSS нет такой возможности.

В идеальном мире


.button {
  display: inline-block;
  padding: 8px 2px;
  border-radius: 3px;
}

.button-disabled {
  composes: button;
  background-color: gray;
}


Все селекторы локальные в рамках конкретного файла.

Это означает, что в файле button.css, я пишу:

.text {...}


А мой коллега в недрах совсем другого компонента:

.text {…}


Нет пересечений для .text — нет необходимости в специальных классах для элементов блоков.

Локальное пространство имен справедливо так же для так же для анимаций, объявленных через @keyframes.

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

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

.button-disabled {
  composes: base from "./base.css";
}


Ключевое слово composes дает мне функционал миксинов.

Причем я могу попросить стили из другого файла, что дает мне модульность на уровне CSS.

Реальность или вымысел


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

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

Представим, что у нас есть простой шаблонизатор, который умеет только интерполяцию строк:

<% var styles = require("./button.css") %>
<button class="<%=styles.button%>">Отправить заявку</button>


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

Сейчас это можно сделать с помощью плагина для webpack или плагина для browserify.

Более современный, реальный пример — в шаблоне reactjs-компонента:

import { Component } from 'react';
import styles from './button.css';

export default class button extends Component {
     render() {
        let className = styles.button
        let text = "Отправить заявку"

        if (this.state.loading) {
            className = styles.buttonDisabled
        }

        return <button className={className}>{text}</button>
    }
}


Что почитать


Кажется, видимая движуха началась с доклада «CSS in JS»

Статья CSS-модули: добро пожаловать в будущее. Прежде, чем читать, откройте исходный код, посмотрите на скомпилированные названия классов: красота! :)

Организация на гитхабе, где ребята штурмуют тему модульности в CSS. Здесь документация: примеры, концепции и конкретные инструменты: postcss, browserify и webpack плагины.

Доклад Павла Ловцевича на последнем WSD (слайды)

Комментарии (34)


  1. musuk
    03.11.2015 17:41

    Мысль интересная, но вообще писать CSS вредно. Человечество уже придумало SCSS и LESS для этих целей. А там можно делать всякие:

    .my-mega-widget {
       .disabled {
           //....
       }
    }
    


    1. ivan386
      03.11.2015 17:46
      +2

      Не вредно

      .button,
      .button-disabled {...}
      
      .button-disabled {...}
      


      1. musuk
        03.11.2015 17:48
        +5

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


    1. vitkarpov
      03.11.2015 18:23

      Имеется ввиду, наверное:

      .my-mega-widget {
        &.disabled {
          // ...
        }
      }
      


      Или disabled — это вложенный элемент, а не модификатор для самого компонента?


      1. gwer
        03.11.2015 19:02
        +1

        Полагаю, имеется в виду подобное:

        .my-mega-widget {
          .button {
            &.disabled {
              // ...
            }
          }
        }
        

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


        1. vitkarpov
          03.11.2015 19:19

          Что случится если внутри my-mega-widget появится my-another-mega-widget внутри которого будет button?

          На button уже будут мачится два селектора: .my-mega-widget .button и .my-another-mega-widget .button, а это не то, чего ожидаешь от композиции компонентов.


          1. gwer
            03.11.2015 19:58
            +2

            Описанная проблема мне ясна.

            Люди занимаются разной работой, пишут разные сайты, разные компоненты. «Жесткие» компоненты с максимальным приоритетом собственных стилей — это круто, когда пишется нечто, что будет использоваться на разных сайтах (в своих проектах или распространяется) и что должно отображаться везде одинаково. И в этом случае тот же БЭМ может оказаться уместным.

            Но таких проектов далеко не большинство. Большинству не нужна эта жесткость, а один и тот же виджет может наоборот требовать разного оформления в зависимости от контекста. И если внутри .my-mega-widget таки понадобится использовать родные стили от .my-another-mega-widget, всегда есть тот же extend, которые пробросит стили и решит все проблемы на уровне приоритетов CSS.

            Я не говорю, что все эти модули/компоненты/БЭМы не нужны. Речь лишь о том, что не нужно их пихать там, где им не место.


            1. vitkarpov
              03.11.2015 21:01

              «Жесткие» компоненты с максимальным приоритетом собственных стилей — это круто, когда пишется нечто, что будет использоваться на разных сайтах


              На любом сайте, который живет и развивается, компонентый подход оправдан.

              Завтра приходит менеджер и просит переставить вот эту штуку сюда, а этот блок вложить вот в этот — не круто говорить в таких случаях «Я так не задумывал! Мне нужно неделю, чтобы все переверстать».


              1. gwer
                03.11.2015 23:49
                +1

                Если на это требуется неделя, значит что-то тут нечисто.


                1. dzhiriki
                  04.11.2015 00:40

                  В этом-то и проблема. Когда один компонент влияет на другой и «разное оформление в зависимости от контекста», то просьба менеджера переставить компонент в другое место превращается в игру «угадай, сломается ли что-нибудь при переносе».
                  И нужно время на то, чтобы разобраться что сломалось и починить.
                  А если бы сразу писались компоненты, которые ни от чего не зависят, проблемы бы даже не возникло.


          1. tenbits
            03.11.2015 20:02
            +1

            Популярный аргумент. Но вообще-то, принцип области ответственности для компонент никто не отменял. Чем конкретнее компонент — тем лучше. Это значит, что он не принимает внутрь посторонии компоненты, если конечно изначально не было это предусмотренно, тогда конечно и css будет другим и разметка. И более того, хорошо когда композиция компонент является плоской, а не вглубину.


          1. stychos
            03.11.2015 23:30
            -1

            В чём вообще проблема?

            .my-mega-widget {
                & > .button {
                    &.disabled {
                        // ...
                    }
                }
            }
            


            1. vitkarpov
              03.11.2015 23:37
              +2

              Проблема в том, что завтра это превратится в

              .my-mega-widget {
                & > .button {
                  & > .text {
                    & > .icon {
                      ...
                    }
                  }
                }
              }
              


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


              1. stychos
                03.11.2015 23:44

                И что, так трудно сфолдить группу и добавить обёртку? Труднее, чем делать то же самое во всяких там БЭМ?


                1. vitkarpov
                  03.11.2015 23:53

                  Кажется, и так и так — сложнее, чем локальный скоуп для каждого компонента по дефолту.


                  1. stychos
                    04.11.2015 02:08

                    Согласен =) Но чем это отличается от обычного нормального владения scss'ом?


          1. Iskin
            04.11.2015 00:19

            Вот тут как раз хороший пример, почему изолировать нужно не только селекторы, но и свойства с помощью `all: initial` и других PostCSS-плагинов.


          1. AmdY
            04.11.2015 02:01

            С чего это будет матчится button?
            если одна кнопка
            .my-mega-widget .button {}
            а вторая совсем в другом неймспейсе
            .my-another-mega-widget .button {}

            <style>
            .mega-widget .button { color: red;}
            .my-another-mega-widget .button { border: solid 1px red;}
            </style>
            <div class="mega-widget"><button class="button">mega-widget</button></div>
            <div class="my-another-mega-widget"><button class="button">my-another-mega-widget</button></div>
            


            При этом используя less можно сделать миксин, чтобы не было дублирования или добавить возможность наследования и расширения.


            1. vitkarpov
              04.11.2015 02:26

              Если вложить один компонент в другой:

              <div class="mega-widget">
                <button class="button">mega-widget</button>
                <div class="my-another-mega-widget">
                  <button class="button">my-another-mega-widget</button>
                </div>
              </div>
              


              Уникальные имена классов для каждого элемента избавляют от таких казусов. В БЭМ это решается неймингом, а в CCS-модулях — автоматически, за счет локального скоупа.


              1. AmdY
                04.11.2015 03:18
                -1

                Чтобы не дублировать http://habrahabr.ru/post/270075/?reply_to=8641825#comment_8641835


                1. BananaBobby
                  04.11.2015 03:23
                  +1

                  Одно дело когда какие-то стили из разряда color идут дальше по дереву и другое, когда специально прописываешь каскады и попадаешь в вышеописанный случай, где верстка кнопок ломается


    1. Iskin
      04.11.2015 00:18

      CSS Modules — это не обычный CSS ;). Это PostCSS, так что подключите postcss-nested и у вас будет та же вложенность.


      1. dzhiriki
        04.11.2015 00:27
        +1

        Так-то, можно писать на том же LESS \ SASS, а потом уже получившуюся CSSку грузить.


        1. Iskin
          04.11.2015 00:28

          Да так тоже можно — но postcss-nested будет быстрее (1 раз парсить) и проще настраивать (не будет проблем с путями карт кода).


    1. Sullenor
      06.11.2015 20:10

      CSS прекрасен своей простотой. Зачем превращать его в язык программирования?

      А по поводу примера, селекторы типа .my-mega-widget с ростом кодовой базы становятся довольно длинными и работать с ними не всегда удобно.
      Я бы сказал, тут интерес другой момент. В эру компонентного подхода можно довольно лаконично описывать состояние компонента без необходимости добавлять какой-то префикс, который гарантирует уникальный неймспейс. Например, с помощь CSS Модулей, туже самую кнопку можно описать как-то так:

      .normal {...}
      .disabled {...}
      .error {...}
      .progress {...}
      


      1. BananaBobby
        07.11.2015 13:19

        В итоге после сборки мы получим все те же .button.error в стилях? По сути ничего от button_error не отличается


        1. Sullenor
          07.11.2015 15:52

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

          Реже возникает необходимость увеличивать специфичность селекторов, так как можно свободно писать .title и не думать о последствиях.


  1. Iskin
    04.11.2015 00:17
    +1

    Зря не упомянули postcss-autoreset или postcss-initial. Изоляция селекторов — только малая часть. Ещё есть большая проблема глобальных ресетов и наследования свойств. Эти плагины их как раз решают.

    Ну а так как CSS Modules тоже написан на PostCSS, то даже парсить два раза не придётся.

    На Front Talks я более подробно рассказывал, как использовать PostCSS для изоляции стилей: www.youtube.com/watch?v=XJaJqLVaR-c


  1. BananaBobby
    04.11.2015 01:12

    Извините, может что-то не понял, но чем это кардинально отличается от бэма, не считая того, что для модификатора не надо писать название изначального блока? (То есть button button_loading button_disabled превратится в button-disabled button-loading)


    1. Iskin
      04.11.2015 01:16
      +1

      Так это и есть просто автоматический БЭМ — не надо ни о чём думать. Примерно как Автопрефиксер и примеси для префиксов.


    1. Iskin
      04.11.2015 01:16

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


    1. vitkarpov
      04.11.2015 01:28

      Я как раз хотел противопоставить в каком-то смысле БЭМ и CSS-модули: есть одна и та же задача — решать ее можно по-разному.

      Как верно отметил Iskin, плюс CSS-модулей в том, что «не надо ни о чем думать».


  1. pepelsbey
    04.11.2015 01:33

    Видео доклада Павла Ловцевича с WSD в Минске.


    1. vitkarpov
      04.11.2015 09:29

      Спасибо, обновил ссылку на доклад