Конец эры глобального CSS

Все 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. Ссылка на оригинальную статью]
Поделиться публикацией
Комментарии 36
    +2
    Модульный CSS реально вещь! Столкнулся с ним недавно при изучении Angular2 на небольшом проекте, очень удобная штука как оказалась: самое важное тут то, что более не нужно хранить контекст селекторов. Т.е. вот в новом ангуляре, ты работаешь с каким-то компонентом интерфейса: у тебя в этом компоненте и логика вся основная тут, и шаблон чисто под компонент, и стили которые применяются только к этому компоненту — это реально круто!

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

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

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

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

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

            Это ложь. Селекторы так и остались глобальными. Просто работать с ними стало удобнее. И за это автору решения большое спасибо. Равно как и переводчику за проделанную работу.
            +1
            Согласен, что звучит громковато и это тоже способ борьбы с существующей проблемой, а не её устранение. Но подход к решению как раз совсем другой — не такой хрупкий, как у существующих «ручных» методик по именованию классов. И он действительно меняет подход к написанию css.

            При разработке, в общем-то, важно, чтобы решение работало. Например, какая разница, используется ли browserify, webpack или нативный браузерный es6 import, если в любом случае мы пишем строчку

            import Module from './Module';
            

            и эта строчка работает? Безо всяких поправок на то, как именно обеспечили её работу. Да, абстракции дают протечки, и хорошо знать, что там под капотом, но это не значит, что абстракции плохая концепция. Ими можно и нужно пользоваться.
            • НЛО прилетело и опубликовало эту надпись здесь
              +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>
              


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

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

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

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

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

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

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

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

                  На счёт импорта согласен, вопрос скользкий, но как по мне, так импорт CSS/JS из HTML как-то более естественно чем CSS из JS + HTML тоже внутри JS. Делает то же самое, но как-то вывернуто всё с ног на голову.
                    0
                    Называется он ShadyDOM, и не дает 100% инкапсуляции. Чисто на бытовом уровне, да это отлично. Сам когда-то любит полимер)
                      0
                      Отчасти вы правы. Но не полностью. Если показать более полный пример:

                      <script src="webcomponents.js"></script>
                      <script>Polymer = {dom: 'shadow'};</script>
                      <link rel="import" href="polymer.html">
                      

                      То будет у вас таки Shadow DOM (полифил для браузеров без нативной поддержки), это то что я имел ввиду, Shady DOM считаю весьма кривым компромиссом и поэтому не использую (имхо).
                        0
                        Я понимаю, но к сожалению на продакшене практически не используется из за малой поддержки и малой производительности на мобильных устройствах. Хоть полифиль хоть не полифиль, полимер еще очень костлявый. По этому комьюнити крутится вокруг того, что уже есть и что работает отлично.
                        как-то более естественно чем CSS из JS + HTML

                        В случае полимера контейнером является html, в случае с react jsx ну или js.
                          0
                          Вы говорите о производительности Polymer 0.5 или 1.x? Хотя по моему опыту даже 0.5 отлично бегает на iOS/Android с нормальным браузером (под нормальным я не имею ввиду встроенный браузер Android < 5)
                            0
                            Да в первом существенно улучшили, но все равно это не сравнимо. Понятное дело моб платформа развивается и будет мощней. Но сейчас, он сыроват и приубавил пыл после google IO. Между прочим на серваке даже отрендерить нельзя, что опять же важно, не говоря уже о размерах билда и нативе :))
                +4
                Судя по статье и примерам, такой подход пытается решить самораздутую проблему с глобальной областью видимости с CSS это всё. При этом, во-первых, нифига её не решает, т.к. единственная разница в конечном CSS, так это названия селекторов в CSS станут короче. Во-вторых отхватите проблемы с кривыми импортами, дублированием импортов или импортом не того и не туда. В-третих, как это все замечательно будет смотреться в девтулзах, когда в браузере на проде ты видишь селектор ._1rJwx92-gmbvaLiDdzgXiJ { … } и не понимаешь откуда он.
                  –1
                  Возможно, вы невнимательно прочитали статью.

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

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

                  При этом, во-первых, нифига её не решает, т.к. единственная разница в конечном CSS, так это названия селекторов в CSS станут короче

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

                  Во-вторых отхватите проблемы с кривыми импортами, дублированием импортов или импортом не того и не туда

                  С чего вдруг? А при импорте .js файлов таких проблем нет? В чем разница?

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

                  Именно этот вопрос в статье описан и решается просто и удобно для дев-режима. А на проде minified, uglified и concatenated javascript вы легко читаете? :)
                    –1
                    Может я не правильно понял автора, мне показалось что проблема с глобальной обласью видимости заключается в 2-ух вещах: первая, то что все висит в глобальной области видимости, вторая, вытекает из первой, то что приходится писать длинные имена, чтобы небыло конфликтов селекторов, например: .button {} для первого виджета и .button {} для второго виджета.
                    Первую проблему описанный подход вообще никак не решает. Вторая проблема решается на уровне облегчения придумывания имен селекторов. Вот второй кейс мне и кажется раздутым.

                    Про импорты ответил чуть ниже.
                      0
                      А с какого размера проектами вы работаете?
                    0
                    Во-вторых отхватите проблемы с кривыми импортами, дублированием импортов или импортом не того и не туда.

                    Это как вообще? Импорты теперь анти-паттерн и опытные разработчики не используют их, чтобы не огрести проблем с импортом «не туда»? :D
                    В-третих, как это все замечательно будет смотреться в девтулзах, когда в браузере на проде ты видишь селектор ._1rJwx92-gmbvaLiDdzgXiJ { … } и не понимаешь откуда он.

                    Source maps
                      0
                      Если смотреть на предлагаемое решение, из него видно, что стили из MyComponent.css импортятся в некий неймспейс 'styles'.
                      Если же говорить об JS импортах, то там разруливание наймспейса идет в самом файле, т.е. вы не сможете запороть неймспейс, например, global.app неправильным импортом, запороть его сможет только код в файле.

                      Source maps — т.е. еще одно усложнение. Уже получается больше сложностей чем облегчений работы.

                        0
                        А разве бывает source map для html?
                      +4
                      Как определять классы если верстка вынесена в отдельный html? К примеру, при использовании Marionette.
                        0
                        Если верстка не статическая, а шаблон, то в него можно прокинуть стили вместе с остальными данными.

                        Например, в Marionette:

                        import {ItemView} from 'backbone.marionette';
                        import styles from './styles.css';
                        
                        export default class extends ItemView {
                        
                            serilalizeData() {
                                // прокидываем стили в шаблон
                                return Object.assign(super.serializeData(), {styles});
                            }
                        }
                        


                        В шаблоне

                        <div class={{styles.foo}}></div>
                        
                        +2
                        Это только для html, создаваемого на стороне клиента? А как быть нормальным людям старикам, которые возвращают уже законченную страницу?

                        Столько пафоса из ничего…
                          +1
                          А также для тех, кто рендерит нодой на сервере.

                          Пафоса многовато, может быть, да, но и не то чтобы «из ничего»
                            +1
                            На самом деле css-modules отдают вам map оригинальных имен на уникальные. Вы можете сохранить их в json и проводить замену на своем сервере, как вам удобно.
                            +1
                            Модульность в CSS таки есть — это инлайн-стили. Тем не менее, каждый проект требует своего подхода.
                              0
                              Спасибо за перевод! Хоть я и считаю предложенный вариант полным трешем, но все равно рад, что все больше становится тем о веб-компонентности. Рад, что к этому движется мир. Глядишь через пару лет кто-то изобретет изящное решение, которое можно будет использовать в продакшене на реальных проектах.
                                +1
                                Пускай сначала к единым стандартам придут и решат проблему кроссбраузерности. :)
                                –1
                                Мне больше нравится вариант полного отказа от CSS и выставления стилей в JS при создании элемента (в React делается очень легко и выглядит очень естественно). Решает все проблемы с CSS.
                                  +1
                                  Кстати на PostCSS вышел не только CSS Modules. Тут есть новая идея — локальный ресет с помощью postcss-autoreset и postcss-intial.

                                  Изолированный CSS означает же не только изолированные имена селекторов. Идеальный изолированный компонент можно переместить в любое место страницы и он будет работать. Но даже с CSS Modules это не так. Следующая проблема после селекторов — наследуемые стили. Например line-height: 0.

                                  Локальный ресет в postcss-autreset вставляет сброс стилей в каждый селектор «блока» или «элемента» (избегая модификаторов).

                                  Я в Париже подробно рассказывал про изоляцию CSS: ai.github.io/postcss-isolation
                                    0
                                    Не совсем понятная проблема. Поэтому выскажу свое предположение:
                                    Каскадные таблицы стилей призваны идти от общего к частному. Поэтому и селекторы в глобальной области видимости, что бы его можно было использовать везде. Иначе размещенный на проекте модуль, который содержит свой индивидуальный CSS и описывает, например, уже используемый на сайте шрифт, ведет к избыточности кода.
                                    Ну и по поводу пересекающихся именований. Что бы селектор выиграл, можно ведь использовать «important», не?
                                      0
                                      Зачем намеренно усложнять вещи, которые призваны быть простыми?
                                      Можно понять возможность расширения функционала, или устранения очевидных проблем вроде позиционирования, кроссбраузерности.
                                      Иначе звучит как «Установить библиотеку из 100 строк, что бы 10 строк писать в 5 строк»

                                      0
                                      Больше нет необходимости лепить длинные префиксы

                                      Что ж он не говорит, что «теперь есть необходимость возможность лепить уникальную вебпаковкую оболочку»? (Про необходимость я не случайно упомянул, потому что появляется ряд компаний, которые услышали звон и начинают компонентную разработку возводить в ранг необходимости.)

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

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

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