Все 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 подходят к концу. Будущее за модульностью.
Автор: everdimension