Все CSS-селекторы живут в глобальной области видимости.

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

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

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

Так не должно быть. Пора оставить позади эру глобальных стилей. Наступило время закрытого CSS.

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

Благодаря таким инструментам, как Browserify, Webpack и jspm фронтенд разработчики получили возможность писать код, состоящий из маленьких модулей, каждый из которых явно запрашивает другие модули, от которых он зависит.

А вот CSS безнаказанно продолжает жить сам по себе.

Многие из нас так привыкли к особенностям CSS, что до недавнего времени не видели других способов решить эту проблему, кроме как ждать поддержки от производителей браузеров. И даже после этого ещё не скоро наступит момент, когда большинство пользователей обзаведутся браузером, полностью поддерживающим Shadow DOM.

Разработчики обходили проблему глобальных классов, предлагая использовать определённую систему соглашений, диктующую, как именно стоит именовать классы. OOCSS, SMACSS, БЭМ, SUIT — все эти методики призваны помочь избежать столкновения пространств имён и сымитировать область видимости.

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

Всё изменилось 22 Апреля 2015

Webpack позволяет импортировать CSS прямо внутри javascript-модуля. Если о таком «трюке» вы слышите впервые, можно почитать подробнее здесь и здесь.

В дело вступает вебпаковский css-loader, позволяющий написать такое:

require('./MyComponent.css');

На первый взгляд выглядит странно. Даже если закрыть глаза на тот факт, что импортируется .css, а не javascript.

Ведь обычно вызов require должен быть сохранён в переменную. Если этого не делают, то, как правило, это говорит о том, что была объявлена глобальная переменная. Явный признак плохой архитектуры.

Но это CSS — глобальной области видимости не избежать. Так считалось ранее.

22 Апреля 2015 года Tobias Koppers — автор Webpack'а — добавил новую фичу в css-loader, и назвал её placeholders. Сейчас она известна как закрытая область видимости.

Эта функция позволяет экспортировать имена классов из CSS файла и запрашивать их внутри нашего javascript'а.

Короче говоря, вместо этого:

require('./MyComponent.css');

Можно написать это:

import styles from './MyComponent.css';

Чем же окажется значение переменной styles? Давайте сначала взглянем, как выглядит сам CSS:

:local(.foo) {
  color: red;
}
:local(.bar) {
  color: blue;
}

В этом примере использован синтаксис, распознаваемый css-loader'ом — :local(.identifier). Такой код экспортирует два идентификатора [назовем их «идентификаторами» в силу уникальности, которая будет обеспечена позже — прим. перев.]: foo и bar.

Эти идентификаторы указывают на имена классов, которые мы и можем использовать в яваскрипте. Вот пример использования с React'ом:

import styles from './MyComponent.css';
import React, { Component } from 'react';

export default class MyComponent extends Component {
  render() {
    return (
      <div>
        <div className={styles.foo}>Foo</div>
        <div className={styles.bar}>Bar</div>
      </div>
    );
  }
}

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

Больше нет необходимости лепить длинные префиксы для каждого селектора, пытаясь имитировать закрытую область видимости. Разные компоненты могут спокойно использовать свои собственные foo и bar, и это не приведёт к столкновению имён.

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

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

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

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

При таком раскладе весь контроль над настоящими именами класса мы возложили на webpack. А это что-то, что поддаётся полной настройке.

По умолчанию, css-loader переводит наши классы в хэши.

Например, такая запись:

:local(.foo) { … }

Будет скомпилирована в такую:

._1rJwx92-gmbvaLiDdzgXiJ { … }

Это не особо удобно во время разработки и отладки. Чтобы генерируемые классы было легче читать, можно задать желаемый формат в конфигурации webpack'а в качестве параметра, передаваемого css-loader'у:

loaders: [
  ...
  {
    test: /\.css$/,
    loader: 'css?localIdentName=[name]__[local]___[hash:base64:5]'
  }
]

И в таком случае наш класс будет скомпилирован вот так:

.MyComponent__foo___1rJwx { … }

Теперь сразу видны и идентификатор, и имя компонента, к которому этот код относится.

А при помощи переменной среды NODE_ENV (environment variable) мы можем разделить логику компиляции для разработки и продакшена:

loader: 'css?localIdentName=' + (
  process.env.NODE_ENV === 'development' ?
    '[name]__[local]___[hash:base64:5]' :
    '[hash:base64:5]'
)

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

Если вы уже придерживаетесь какой-либо методики по созданию пространства имён, например, БЭМом, то перевести весь css код на изолированные стили будет простым и логичным действием.

Вскоре можно будет обнаружить, что большинство CSS файлов использует исключительно закрытые идентификаторы:

:local(.backdrop) { … }
:local(.root_isCollapsed .backdrop) { … }
:local(.field) { … }
:local(.field):focus { … }
etc.…

Потребность же в глобальных классах возникает лишь иногда. Отсюда напрашивается мысль:

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

Что если наш код будет всё-таки выглядеть так:

.backdrop { … }
.root_isCollapsed .backdrop { … }
.field { … }
.field:focus { … }

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

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

Так, например, будет выглядеть запись, использующая стандартные классы, добавляемые аддоном ReactCSSTransitionGroup:

.panel :global .transition-active-enter { … }

Этот код создаёт приватный идентификатор .panel, который опирается на глобальный класс .transition-active-enter

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

На помощь пришёл PostCSS — прекрасный инструмент для написания собственных CSS преобразователей в виде плагинов. Например, популярнейший Autoprefixer — изначально как раз является PostCSS плагином, сейчас используемый многими как самостоятельный инструмент.

Далее автор оригинальной статьи кратко описывает свою экспериментальную библиотеку, осуществляющую задумку. Вот пример её использования. Идеи автора позже были были приняты сообществом и интегрированы в сам webpack. Технологию назвали CSS Modules, которая стала частью css-loader'а. Экспериментальный проект больше не актуален. Итоговый пример использования CSS Modules здесь

Изолированные css классы — это только начало.

Эй, ты починил css, — tweet

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

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

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

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

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



[Публикация — перевод. Автор статьи Mark Dalgleish. Ссылка на оригинальную статью]

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


  1. biziwalker
    02.02.2016 13:44
    +2

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

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


    1. everdimension
      02.02.2016 15:02
      +1

      А не так давно все рассчитывали, что эту проблему решит Shadow DOM.

      Затем появилась связка virtual dom + es6/browserify imports, и всё, чего ей не хватало, это изолированные стили.
      Теперь, возможно, и так неспешное внедрение shadow dom'а затянется ещё сильнее. Посмотрим!


  1. szubtsovskiy
    02.02.2016 15:13
    +24

    Это, безусловно, круто и нужно. Напрягает только кривление (или кривляние?) душой: ведь по сути проблема глобальной природы CSS никуда не делась, и это решение — просто ещё один БЭМ, только автоматизированный. Очередной костыль. А раз костыль, то, по закону дырявых абстракций, где-нибудь он вылезет боком.
    Я люблю webpack и, скорее всего, попробую использовать этот инструмент, но, все-таки, очень хочется, чтобы вещи называли своими именами, не превознося один способ обходить ограничения технологий над другим.


  1. nazarpc
    02.02.2016 16:00
    +19

    Вот это жесть… К смешанному HTML и JavaScript домешали немного CSS с кастомным синтаксисом — и получилось вообще непонятно что.

    Используйте веб-компоненты — естественный способ создания элементов, где HTML, JavaScript, CSS лежать отдельно, CSS не находится в глобальной области видимости и всё работает по стандартах без всяких WebPack.

    С такими системами сборки можно сломать всё — сначала ноги, потому руки, а в конце и голову.
    Если хотите всё в кучу намешать — что ж, Polymer позволяет и так, хотя четкое разделение всё же остается, просто в одном файле:

    <dom-module id="my-element">
        <template>
            <style>
                .super {color: red;}
            </style>
            <div class="super">Hello, red!</div>
        </template>
        <script>
            Polymer({
                is : 'my-element'
            });
        </script>
    </dom-module>
    


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

    Не усложняйте веб, всё может быть гораздо проще.


    1. everdimension
      02.02.2016 16:26

      Веб-компоненты — отличная идея, а <template></template> с собтвенным тэгом <style></style> — офигительно удобно.

      Проблема в том, что до их внедрения ещё не скоро. Shadow DOM — штука, под которую практически невозможно сделать полифилл.

      А технология, описываемая в статье, будет работать прямо сейчас в любом браузере. Нужна настройка системы сборки? Да. Но ведь и Polymer требует того, чтобы над ним посидеть и всё настроить.

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

      А импорт css внутри javascript модуля я бы не назвал «мешаниной». Это идея цельного компонента. Ингредиенты: html, css, javascript. Вполне логично, когда они вместе. Web components ведь о том же говорят :)


      1. nazarpc
        02.02.2016 16:49
        +2

        На самом деле полифил уже есть несколько лет как и используется на многих сайтах в продакшене. Да, он не идеален, но работает очень даже хорошо. И для работы Polymer не нужно ничего настраивать, вся настройка заканчивается на одной строчке:

        <link rel="import" href="polymer.html">
        

        То есть системы сборки вы можете использовать (тот же Vulcanize упакует ваши HTML импорты), но это не является требованием, в этом радикальное отличие. Система сборки хорошая штука до тех пор, пока вы сами решаете нужна вам она или нет и когда вы можете выбрать ну систему сборки, которую хотите.

        На счёт импорта согласен, вопрос скользкий, но как по мне, так импорт CSS/JS из HTML как-то более естественно чем CSS из JS + HTML тоже внутри JS. Делает то же самое, но как-то вывернуто всё с ног на голову.


  1. eme
    02.02.2016 17:24
    +4

    Судя по статье и примерам, такой подход пытается решить самораздутую проблему с глобальной областью видимости с CSS это всё. При этом, во-первых, нифига её не решает, т.к. единственная разница в конечном CSS, так это названия селекторов в CSS станут короче. Во-вторых отхватите проблемы с кривыми импортами, дублированием импортов или импортом не того и не туда. В-третих, как это все замечательно будет смотреться в девтулзах, когда в браузере на проде ты видишь селектор ._1rJwx92-gmbvaLiDdzgXiJ { … } и не понимаешь откуда он.