Так ли уж нужна специфичность в CSS?

Перевод статьи Do We Actually Need Specificity In CSS? с сайта philipwalton.com, c разрешения автора — Филипа Уолтона.

Окей, прежде чем начать, я хочу заранее прояснить одну вещь. Эта статья — не напыщенные фразы о том, как сильно я ненавижу специфичность. Если нужна статья об этом, то уверен, что в сети таких полно.

Цель моей статьи — задать актуальный и откровенный вопрос сообществу веб-разработчиков, и, надеюсь, заставить людей задуматься.

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

Теперь, я уверен, что некоторые подумали: какая разница? Специфичность есть, и мы привязаны к ней. Так какой смысл заводить этот разговор?

Для тех, кто так думает, рад сообщить, что вы ошибаетесь :-)

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

Небольшая предыстория

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

Исходный порядок очень важен. Вполне естественно и интуитивно полагать, что если правило Y следует после правила X, и оба правила применяются к одному элементу, то «побеждают» объявления Y.

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

Но специфичность, я считаю, — это нечто иное.

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

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

window.document.foo = 'bar';
document.foo = 'qux';

assert(window.document.foo == 'bar'); // true, Какого чёрта?

Было бы безумным и полностью неконтролируемым, если бы присваивание выше window.document.foo = 'bar' (которое идёт первым), перебивало бы присваивание document.foo = 'qux' (которое идёт вторым) только потому, что ссылка «более специфична». Тем не менее, это по сути то, что происходит в CSS.

Но специфичность полезна, не правда ли?

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

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

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

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

Другими словами, мне никогда не приходилось делать что-то вроде примера ниже, где более специфичные ссылки в подвале определены до менее специфичных ссылок по умолчанию:

.footer a {
  color: white;
  text-decoration: none;
}

a {
  color: blue;
  text-decoration: underline;
}

Если мне нужно, чтобы правило Y переопределялось правилом X, то я вставляю последнее позже по порядку в исходнике. И баста. Это значит, что теперь любой конфликт специфичностей — всегда катастрофа.

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

По сути, что, если бы я мог уберечь себя (и своих коллег) от случайного отхода от парадигмы, которая, согласитесь, имеет огромный смысл.

Что, если бы специфичности не существовало?

Представим мир без специфичности в CSS. В таком мире каскад определялся бы только порядком в исходном коде и важностью. И, если правила Х и Y применяются к одному элементу, а правило Y идёт после Х, то независимо от специфичности правило Y всегда «побеждает». Единственный способ для свойства в правиле Х переопределить свойства в правиле Y — использовать важность.

Был бы мир лучше? Станет ли код более предсказуемым, легче поддерживаемым и расширяемым?

Пример из реальной жизни

Множество людей использует в CSS классы «состояния» или «полезности». Например, класс is-hidden, который (обычно) используется для скрытия элемента.

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

Если бы специфичности не существовало, то вы бы гарантировали, что классы состояния победят другие классы, просто добавив их последними в исходный код. Но чтобы решить эту проблему сегодня, пришлось бы использовать !important.

Кстати, что интересно, добавление в таких ситуациях !important на самом деле рекомендуется при использовании классов состояния в SMACSS и классов полезности в SUIT. Было бы неплохо, если бы нашим передовым методам не приходилось прибегать к крайним мерам для повседневных нужд стилизации.

Удаление специфичности из каскада

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

Какой? Ответ — сделать специфичность и порядок в коде одинаковыми.

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

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

Но что, если бы транспилер мог изменять CSS после его написания, гарантируя, что специфичность всех селекторов идёт в нарастающем порядке? И что ещё важнее, что, если бы транспилер при этом ещё и не затрагивал то, каким элементам соответствуют эти селекторы?

Благодаря некоторым удивительным особенностям CSS-селекторов, это возможно!

Рассмотрим следующие CSS-правила, теперь с нисходящим порядком специфичности (вверху каждого правила я перечислил специфичность с помощью нотации [число id].[число классов].[число тегов])

/* 0.2.5 */
main.content aside.sidebar ul li a { }

/* 0.1.3 */
aside.sidebar ul a { }

/* 0.1.1 */
.sidebar a { }

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

/* 0.2.5 */
main.content aside.sidebar ul li a { }

/* 0.3.3 */
:root:root aside.sidebar ul a { }

/* 0.4.1 */
:root:root:root .sidebar a { }

Это работает, поскольку в любом HTML-документе есть корневой элемент (элемент <html>), так что добавление псевдокласса :root в начало селектора не изменит то, каким элементам этот селектор может соответствовать.

А поскольку псевдоклассы могут идти цепочкой (т.е. :root:root:root будет по-прежнему соответствовать элементу <html>), то можно произвольно добавлять специфичность к любому селектору, делая его специфичнее предыдущего.

Укрощение специфичности ID

Селекторы по ID специфичнее псевдоклассов, а следовательно никакие добавления :root к селектору не смогут переопределить ID.

Однако, селектор по ID #content и селектор атрибута [id="content"] будут соответствовать в точности одному элементу, поэтому, если заменить все селекторы по ID на селекторы атрибута, то описанная выше техника по-прежнему будет работать.

/* 1.1.5 */
main#content aside.sidebar ul li a { }

/* 0.1.3 */
aside.sidebar ul a { }

/* 0.1.1 */
.sidebar a { }

Будут транспилированы в:

/* 0.2.5 */
main[id="content"] aside.sidebar ul li a { }

/* 0.3.3 */
:root:root aside.sidebar ul a { }

/* 0.4.1 */
:root:root:root .sidebar a { }

Обращение к селекторам, которые, возможно, уже ссылаются на корневой элемент

Глядя на большинство селекторов, невозможно точно определить, собираются ли они соответствовать элементу <html>. Например, если на сайте используется Modernizr, то селекторы, вероятно, будут выглядеть так:

.columns {
  display: flex;
}

.no-flexbox .columns {
  display: table;
}

Чтобы учесть вероятность того, что любой из этих селекторов может соответствовать элементу <html>, вам пришлось бы включить обе возможности и переписать их таким образом:

:root.columns,
:root .columns {
  display: flex;
}

:root.no-flexbox .columns,
:root .no-flexbox .columns {
  display: table;
}

Другой вариант, как можно избежать этой проблемы полностью — принять соглашение, что ни одному из ваших селекторов не разрешается соответствовать элементу <html>, и что <body> — самый вышестоящий элемент, до которого могут добираться селекторы.

Алгоритм полного переписывания

Я не знаю ни одного транспилера, который делал бы то, что я здесь описал. Если бы вы заинтересовались в написании такого транспилера, то я рекомендовал бы пойти по пути плагина PostCSS. Поскольку множество алгоритмов сборки уже включают в себя Autoprefixer (плагин PostCSS), добавление еще одного почти не повлияет на время сборки или сложность.

Вот какому базовому алгоритму пришлось бы следовать:

  1. Перебираем каждый селектор в таблице стилей
  2. Для каждого селектора:
    1. Если селектор содержит любые селекторы по ID, то заменяем их на селекторы атрибутов по ID, иначе ничего не делаем.
    2. Вычисляем специфичность текущего селектора (со всеми заменёнными ID)
    3. Если текущий селектор является первым, ничего не делаем. Иначе, сравниваем его специфичность со специфичностью предыдущего селектора.
    4. Если предыдущий селектор менее специфичен чем текущий, ничего не делаем. Иначе, переписываем текущий селектор, чтобы его специфичность была выше или равна специфичности предыдущего селектора.
      1. Если текущий селектор начинается с селектора :root или селектора типа, отличного от html, переписываем селектор с :root вначале в качестве предка. Иначе переписываем селектор, чтобы в первой части указывалась и цепочка с :root в начале селектора, и селектор в качестве потомка :root.
  3. После того, как все селекторы переписаны, выводим финальные стили. Их специфичность теперь будет идти в порядке возрастания.

Потенциальные недостатки

Хотя описанный мною подход безусловно может работать, у него есть несколько недостатков. С одной стороны, он увеличивает размер CSS-файла. Что едва ли станет проблемой, если специфичность у вас уже низкая, и вы стараетесь писать правила в порядке возрастания специфичности. А поскольку кучу раз добавляется только лишь слово «root», оно должно хорошо сжиматься gzip-ом, так что, думаю, разница будет незначительной.

С другой стороны, надо учитывать потенциальные проблемы с обращением к внешним стилям. Если подключить bootstrap.css из CDN, а затем применить эту технику к другим стилям, то могут быть странности, поскольку один набор селекторов будет переписан, а другой — нет.

И не факт, что у вас получится просто включить bootstrap.css в вашу сборку, поскольку эта возможность зависит от корректной работы специфичности.

Заключительные мысли

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

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

Если есть большая команда, которая постоянно сражается со специфичностью, то было бы интересно узнать, помогло ли ей что-то подобное. Если у вас получится это сделать, то не стесняйтесь, поделитесь результатами. А ещё лучше, напишите ответную статью, и я сошлюсь на неё.

Обновление (от 03.11.2015)

Лиа Веру указала мне в твиттере, что в качестве альтернативы :root вы могли бы использовать псевдокласс :not() для произвольного увеличения специфичности селектора.

Преимущество :not() заключается в том, что его можно применять к любому элементу (включая <html>), поэтому вам бы не пришлось разделять селекторы. Также его можно использовать для добавления специфичности уровня тега или ID, напр. :not(z) или :not(#z), так что вам не пришлось бы всегда прибегать к помощи классов.

Недостаток :not() в том, что вы должны тщательно подбирать селектор, который гарантировано не совпадает с элементом, иначе он не будет работать.

P.S. Это тоже может быть интересно:

4 Комментарии

  1. Alnat2


    Вполне естественно и интуитивно полагать, что если правило Х следует после правила Y, и оба правила применяются к одному элементу, то «побеждают» объявления Y.

    По-моему X и Y следует поменять местами)

    1. SelenIT

      И ведь действительно:). Спасибо, исправили!

  2. Мимо проходил

    Никогда проблем со специфичностью не было.

  3. Рафис

    Спасибо за перевод.
    Всегда стараюсь писать так, как говорится в этой статье. Сначала менее специфичные, а потом более специфичные. Короче говоря, не нужно проходиться этим «плагином» по моим стилям.
    А вообще, если использовать БЭМ и стараться делать без вложенных селекторов, то проблема становится еще менее значительной.
    Но я верстаю Sharepoint порталы, поэтому там конечно не все идеально с БЭМ и со специфичностью. Иногда добавляются файлы стилей уже после моих файлов и приходится «побеждать» их со специфичностью

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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