CSS-модули

    Какими 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 (слайды)
    Поделиться публикацией

    Похожие публикации

    Комментарии 39
      0
      Мысль интересная, но вообще писать CSS вредно. Человечество уже придумало SCSS и LESS для этих целей. А там можно делать всякие:
      .my-mega-widget {
         .disabled {
             //....
         }
      }
      
        +2
        Не вредно
        .button,
        .button-disabled {...}
        
        .button-disabled {...}
        
          +5
          Ну это же страшно выглядит и сложно сопровождается.
          Ведь есть няшные LESS/SCSS с миксинами, переменными, и красивым иерархичным, читабельным кодом.
          0
          Имеется ввиду, наверное:

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


          Или disabled — это вложенный элемент, а не модификатор для самого компонента?
            +1
            Полагаю, имеется в виду подобное:
            .my-mega-widget {
              .button {
                &.disabled {
                  // ...
                }
              }
            }
            

            Это как раз то самое пространство имен, об отсутствии которого все постоянно плачут и уходят на три буквы.
              0
              Что случится если внутри my-mega-widget появится my-another-mega-widget внутри которого будет button?

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

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

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

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


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

                  Завтра приходит менеджер и просит переставить вот эту штуку сюда, а этот блок вложить вот в этот — не круто говорить в таких случаях «Я так не задумывал! Мне нужно неделю, чтобы все переверстать».
                    +1
                    Если на это требуется неделя, значит что-то тут нечисто.
                      0
                      В этом-то и проблема. Когда один компонент влияет на другой и «разное оформление в зависимости от контекста», то просьба менеджера переставить компонент в другое место превращается в игру «угадай, сломается ли что-нибудь при переносе».
                      И нужно время на то, чтобы разобраться что сломалось и починить.
                      А если бы сразу писались компоненты, которые ни от чего не зависят, проблемы бы даже не возникло.
                  +1
                  Популярный аргумент. Но вообще-то, принцип области ответственности для компонент никто не отменял. Чем конкретнее компонент — тем лучше. Это значит, что он не принимает внутрь посторонии компоненты, если конечно изначально не было это предусмотренно, тогда конечно и css будет другим и разметка. И более того, хорошо когда композиция компонент является плоской, а не вглубину.
                  • НЛО прилетело и опубликовало эту надпись здесь
                      +2
                      Проблема в том, что завтра это превратится в

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


                      А послезавтра потребуется добавить обертку для кнопки и селектор непосредственного потомка перестанет работать.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Кажется, и так и так — сложнее, чем локальный скоуп для каждого компонента по дефолту.
                          • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Вот тут как раз хороший пример, почему изолировать нужно не только селекторы, но и свойства с помощью `all: initial` и других PostCSS-плагинов.
                        0
                        С чего это будет матчится 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 можно сделать миксин, чтобы не было дублирования или добавить возможность наследования и расширения.
                          0
                          Если вложить один компонент в другой:

                          <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
                            Чтобы не дублировать http://habrahabr.ru/post/270075/?reply_to=8641825#comment_8641835
                              +1
                              Одно дело когда какие-то стили из разряда color идут дальше по дереву и другое, когда специально прописываешь каскады и попадаешь в вышеописанный случай, где верстка кнопок ломается
                    0
                    CSS Modules — это не обычный CSS ;). Это PostCSS, так что подключите postcss-nested и у вас будет та же вложенность.
                      +1
                      Так-то, можно писать на том же LESS \ SASS, а потом уже получившуюся CSSку грузить.
                        0
                        Да так тоже можно — но postcss-nested будет быстрее (1 раз парсить) и проще настраивать (не будет проблем с путями карт кода).
                      0
                      CSS прекрасен своей простотой. Зачем превращать его в язык программирования?

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

                      .normal {...}
                      .disabled {...}
                      .error {...}
                      .progress {...}
                      
                        0
                        В итоге после сборки мы получим все те же .button.error в стилях? По сути ничего от button_error не отличается
                          0
                          Да, на выходе результат схожий, но с исходными файлами становится чуть проще работать за счет подобной автоматизации. Также уменьшаются возможные коллизии, так как задача генерации уникальных селекторов перекладывается на роботов.

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

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

                      На Front Talks я более подробно рассказывал, как использовать PostCSS для изоляции стилей: www.youtube.com/watch?v=XJaJqLVaR-c
                      0
                      Извините, может что-то не понял, но чем это кардинально отличается от бэма, не считая того, что для модификатора не надо писать название изначального блока? (То есть button button_loading button_disabled превратится в button-disabled button-loading)
                        +1
                        Так это и есть просто автоматический БЭМ — не надо ни о чём думать. Примерно как Автопрефиксер и примеси для префиксов.
                          0
                          Там, правда, есть ещё один плюс — в конец селектора добавляется случайная строка. Но это важно только для разработки с виджетов для стороннего сайта, где может произойти конфликт имени блока.
                            0
                            Я как раз хотел противопоставить в каком-то смысле БЭМ и CSS-модули: есть одна и та же задача — решать ее можно по-разному.

                            Как верно отметил Iskin, плюс CSS-модулей в том, что «не надо ни о чем думать».
                            0
                            Видео доклада Павла Ловцевича с WSD в Минске.
                              0
                              Спасибо, обновил ссылку на доклад
                              0
                              А что если, вместо БЭМ и модулей принять соглашение о том, что каждый виджет должен быть изолирован своим «главным» классом?

                              .awesome-module {
                              	.button {
                              		&.disabled {
                              			//...
                              		}
                              	}
                              }
                              
                              .cool-module {
                              	.button {
                              		&.disabled {
                              			//...
                              		}
                              	}
                              }
                              


                              Мне нравится идея изоляции CSS, но я не понимаю, зачем такие костыли.
                                0
                                А теперь представьте, что у вас cool-module расположен внутри awesome-module. Тогда стили внутренних элементов будут накладываться друг на друга.
                                  0
                                  Действительно, такая изоляция может работать, но только для атомарных компонентов: когда мы уверены, что внутри ничего не появится (того, что мы не задумывали).

                                  Во всех остальных случаях необходимо обеспечить, чтобы селекторы элементов одного блока не мачились на вложенный, потому что сам по себе селектор .cool-module .button не обеспечивает этого (будет мачится на все .button внутри, даже если это уже кнопка из другого, вложенного, блока)

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое