Pull to refresh

Инкапсуляция CSS-стилей — Часть 1. Проблема

Reading time 5 min
Views 44K
Главным драйвером роста веба на рубеже тысячелетий было потребление контента. Сайты создавались для предоставления своим посетителям какой-либо полезной информации или развлекательного содержимого. Но в последние годы резко выросло значение веб-ресурсов, предоставляющих пользователям сервисы генерации контента (текстовые и графические редакторы, электронные таблицы, мессенджеры и т.п.). Это вызвало трансформацию сайтов в одностраничные приложения и миграцию в веб сложных интерфейсов, которые ранее были прерогативой прикладных программ.

В процессе этих трансформаций и миграций выяснилось, что рост трудоемкости создания и поддержания веб-интерфейса значительно опережает рост его сложности. Проблему пытались (и пытаются до сих пор) решить путем разбиения на модули, абстрагирования, инкапсуляции. С этой целю было создано большое количество JavaScript-фреймворков (Backbone, Ember, Angular), HTML-шаблонизаторов (Jade, Handlebars), систем управления зависимостями (RequireJS) и т.п.

Наиболее сложным с этой точки зрения оказался CSS, где по дизайну языка любое свойство, объявленное в любом подключенном CSS-файле или тэге style, может повлиять на отображение любого элемента DOM-дерева.

Формализация задачи.

Предположим для простоты, что весь JavaScript-код заключен в модули, которые ничего не знают друг о друге и инкапсулируют в себе все необходимое им для своей работы. Модуль знает как сгенерить свое HTML-представление (назовем его блок) и куда его вставить в DOM-дереве. При этом блоки могут вкладываться друг в друга. На уровне разметки блок состоит из корневого элемента и дочерних элементов.
Задача заключается в том, чтобы на отображение элементов любого блока можно было повлиять только намеренным изменением в HTML-представлении и соответствующих CSS-файлах и тэгах style.

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

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

Иточники протечек стилей

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

1. Наследование свойств

При отсутствии у CSS-свойства какого-либо элемента явно заданного значения используется значение по умолчанию. Если это значение inherit, то значение свойства задается равным значению свойства родительского элемента.
Таким образом, корневой элемент текущего блока может наследовать стили своего родителя, по определению принадлежащего другому блоку.

К примеру,
.outer-block{
    color: red;
}

<div class="outer-block">
    Я намеренно сделан красным
    <div class="current-block">
        Я красный из-за наследования стилей
    </div>
</div>

посмотреть на jsfiddle.net

2. Конформизм свойств

При отсутствии у CSS-свойства какого-либо элемента явно заданного значения используется значение по умолчанию. Если это свойство подстраивается под свойство родительского элемента (к примеру, как свойства width и height со значением по умолчанию auto) или мимикрирует (к примеру, как свойство background-color со значением по умолчанию transparent), то у конечного пользователя будет создаваться впечатление, что стили родительского элемента протекли на стили дочернего элемента.

К примеру,
.outer-block{
    background: red;
}

<div class="outer-block">
    Я намеренно сделан красным
    <div class="current-block">
        Я красный из-за конформизма стилей
    </div>
</div>

посмотреть на jsfiddle.net

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

3. Каскадный беспредел

Применение стилей к целевым элементам селекторов происходит в три этапа.
На первом этапе из всего DOM-дерева выбираются все элементы, соответствующие селектору целевого элемента. К примеру, для селектора .current-block h3 на первом этапе будут выбраны все элементы с тэгом h3. Способа ограничить пространство выбора каким-либо участком DOM-дерева не существует.
На втором этапе выбранные элементы фильтруются на предмет соответствия селектору путем обхода родительских элементов целевого элемента. При использовании комбинатора потомка пробел (descendant combinator) поиск соответствия может идти вплоть до корневого элемента DOM-дерева. При использовании сестринского комбинатора ~ (general sibling combinator) — до самого первого сестринского элемента.

К примеру,
.current-block h3 {
    background: blue;
}
.outer-block h3 {
    background: red;
}

<div class="outer-block">
    <h3>Я намеренно сделан красным</h3>
    <div class="current-block">
        <h3>Я красный из-за каскадного беспредела</h3>
    </div>
</div>

посмотреть на jsfiddle.net

Единственным способом ограничить пространство поиска является использование дочернего комбинатора > (child combinator) и ближайшего сестринского комбинатора + (adjacent sibling combinator). Для этого необходимо задавать точный путь в DOM-дереве от целевого элемента к корневому элементу блока, что приводит к увеличению связанности CSS и HTML-кода.

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

Таким образом, при определенных условиях (используемые комбинаторы, соотношение специфичностей, порядок объявления) стили элементов внешнего блока могут влиять на стили элементов вложенного блока. Этого влияния можно было бы избежать, если бы можно было указывать область DOM-дерева, в которой следует выбирать и фильтровать целевые элементы.

4. Позиционная обусловленность

Элементы блоков могут менять свое отображение в зависимости от позиции блока в DOM-дереве при использовании в селекторах сестринских комбинаторов (+ и ~) или псевдоклассов (:first-child и т.п.).

К примеру,

.block {
    background: red;
}
.block + .block {
    background: blue;
}

<div class="block">
    <h3>Я красный, но при добавлении блока передо мной я стану синим</h3>
</div>
<div class="block">
    <h3>Я синий</h3>
</div>

посмотреть на jsfiddle.net

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

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

Вторая часть статьи («Инкапсуляция CSS-стилей — Часть 2. Решения») будет посвящена анализу того, насколько текущие подходы (OOCSS, SMACSS, ACSS, BEM, CSS-препроцессоры) соответствуют идеалу, а также их классификации.

Буду рад полезным советам и конструктивной критике.
Tags:
Hubs:
+26
Comments 14
Comments Comments 14

Articles