CSS-live.ru

О семантике HTML и архитектуре веб-фронтенда

Перевод статьи Николаса Галлахера «About HTML semantics and front-end architecture», переводчик — SelenIT

Собрание мыслей, опыта, идей, которые мне нравятся и идей, с которыми я экспериментировал последний год. Они охватывают семантику HTML, компоненты и подходы в архитектуре веб-фронтенда, способы именования классов и HTTP-сжатие.

Мы не прервем своих исканий,
И под конец всех странствий наших
Придем туда, откуда вышли,
Узнав впервые этот край.

Т. С. Элиот — «Маленький Гиддинг»

О семантике

Семантика — учение об отношениях между знаками и символами и тем, что они обозначают. В лингвистике это главным образом учение о значении знаков (таких как слова, фразы или звуки) в языке. В контексте клиентской веб-разработки семантика по большей части соотносится с общепринятым значением HTML-элементов, атрибутов и их значений (включая расширения типа микроданных). Эта общепринятая семантика, которая обычно формализована в спецификациях, может помочь программам (и, следовательно, людям) лучше понять отдельные аспекты информации на сайте. Однако даже после формализации эта семантика элементов, атрибутов и их значений может быть по-своему адаптирована и принята разработчиками. Это может привести к последующим изменениям формально утвержденной семантики (и это — один из принципов разработки HTML, известный как "мостить проторенные тропы").

Как различать разные типы семантики HTML

Принцип написания "семантичного HTML" — один из столпов современной профессиональной клиентской веб-разработки. Большая часть семантики относится к «природе» имеющегося или ожидаемого контента (напр. элемент h1, атрибут lang, значение email атрибута type или микроданные).

Однако не вся семантика должна быть производной от контента. Имена классов не могут быть «несемантичными». Для чего бы классы ни использовались — у них есть значение, есть цель. Семантика имен классов может отличаться от таковой для HTML-элементов. Мы можем вовсю пользоваться общепринятой «глобальной» семантикой элементов и определенных атрибутов HTML, микроданных и т.п., не путая их задачи с задачами «локальной» семантики, специфичной для данного сайта или приложения, которая обычно заключена в атрибутах типа class.

Несмотря на повторяющееся в разделе спецификации HTML5 о классах допущение о «хорошей практике», согласно которой…

…авторам рекомендуется использовать значения [атрибута class], описывающие контент, а не значения, опиывающие желаемое представление контента.

…нет ни одной объективной причины так делать. На самом деле, это часто лишь помеха в работе над большими сайтами или приложениями.

  • Контентная семантика уже выражена элементами и другими атрибутами HTML.
  • Имена классов не сообщают почти никакой (или вовсе никакой) смысловой информации ни машинам, ни людям, если только они не входят в ограниченное множество общепринятых (и машиночитаемых) имен — микроформаты.
  • Основное назначение имен классов — привязка для CSS и JavaScript. Если вам не нужно добавлять к веб-документу представление и поведение, вам, вероятно, и классы в HTML не нужны.
  • Имена классов должны передавать полезную информацию разработчикам. Полезно понимать, что делает то или иное имя класса, при чтении фрагмента DOM, особенно в командах из множества разработчиков, где, кроме специалистов по клиентской стороне, с HTML-компонентами могут работать и другие люди.

Возьмем вот такой простейший пример:

<div class="news">
    <h2>News</h2>
    [контент новости]
</div>

Имя класса news не говорит ничего сверх того, что и так очевидно из контента. Оно не дает никакой информации о структуре компонента, и не может быть использовано с контентом, не являющимся «новостями». Жесткая привязка имен классов к природе контента уже делает вашу архитектуру менее  масштабируемой и затрудняет взятие ее на вооружение другими разработчиками.

Контентно-независимые имена классов

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

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

Архитектура клиентской части

Цель компонентно-/шаблонно-/объектно-ориентированной архитектуры — иметь возможность создать ограниченное число повторно используемых компонентов, которые могут содержать целый диапазон разных типов контента. Важная вещь в семантике имен классов для нетривиальных приложений — они должны диктоваться прагматизмом и наилучшим образом служить своей первоочередной задаче: предоставлять понятные, гибкие и многоразовые презентационные и поведенческие «зацепки» для использования разработчиками.

Компоненты, которые можно использовать повторно и комбинировать

Масштабируемый HTML/CSS должен, вообще говоря, полагаться на классы в HTML, чтобы создаваемые компоненты действительно можно было использовать повторно. Гибкий и многоразовый компонент — это такой, который ни полагается на определенную часть DOM-дерева внутри себя, ни требует элементов определенного типа. Он должен подстраиваться под разные контейнеры и легко менять темы оформления. Если необходимо, для большей надежности компонента можно использовать и дополнительные HTML-элементы, сверх того, что нужно просто для разметки контента. Хороший пример — то, что Николь Салливэн называет медиаобъектом.

Компоненты, которые легко можно комбинировать друг с другом, выигрывают от отказа от селекторов по типу в пользу классов. В следующем примере непросто объединить компоненты btn и uilist. Проблема в том, что специфичность селектора .btn ниже, чем у .uilist a (который перекрывает общие свойства), и в том, что компонент uilist требует, чтобы внутри у него были именно ссылки.

.btn { /* стили */ }
.uilist { /* стили */ }
.uilist a { /* стили */ }
<nav class="uilist">
    <a href="#">Домой</a>
    <a href="#">О нас</a>
    <a class="btn" href="#">Вход</a>
</nav>

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

.btn { /* стили */ }
.uilist { /* стили */ }
.uilist-item { /* стили */ } /* ключевой момент */
<nav class="uilist">
    <a class="uilist-item" href="#">Домой</a>
    <a class="uilist-item" href="#">О нас</a>
    <span class="uilist-item"><!-- обратите внимание на имя тега -->
        <a class="btn" href="#">Вход</a>
    </span>
</nav>

Классы, связанные с JavaScript

Использование связанных с JavaScript классов (в том или ином виде) может помочь снизить риск того, что изменения структуры или оформления компонента поломают функциональность связанного с ним скрипта. Подход, который я нахожу полезным — использовать определенные классы только для привязки JavaScript js-* — не навешивая на них никакого оформления.

<a href="/login" class="btn btn-primary js-login"></a>

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

Модификаторы компонентов

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

«Одноклассовый» паттерн

.btn, .btn-primary { /* стили шаблона кнопки */ }
.btn-primary { /* стили, специфичные для кнопки входа */ }
<button class="btn">Default</button>
<button class="btn-primary">Login</button>

«Многоклассовый» паттерн

.btn { /* стили шаблона кнопки */ }
.btn-primary { /* стили специфичные для основной кнопки */ }
<button class="btn">Default</button>
<button class="btn btn-primary">Login</button>

Если вы пользуетесь препроцессором, вы можете воспользоваться функциональностью @extend из Sass, чтобы тратить меньше сил на поддержку решения с «одноклассовым» паттерном. Однако, даже при наличии препроцессора, я предпочитаю использовать «многоклассовый» паттерн и добавлять классы-модификаторы в HTML.

Я пришел к выводу, что он лучше масштабируется. Например, возьмем базовый объект btn и добавим еще 5 типов кнопок и 3 добавочных размера. С «многоклассовым» паттерном вы ограничитесь 9 классами, которые можно будет смешивать и комбинировать. С «одноклассовым» вам понадобятся 24 класса.

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

/* "многоклассовая" подстройка */
.thing .btn { /* правки стилей */ }

/* "одноклассовая" подстройка */
.thing .btn,
.thing .btn-primary,
.thing .btn-danger,
.thing .btn-etc { /* правки стилей */ }

«Многоклассовый» паттерн предполагает лишь один «внутрикомпонентный» селектор, чтобы «достучаться» до любого объекта на базе btn в компоненте. С «одноклассовым» пришлось бы заводить отдельный класс для каждого возможного типа кнопки и заново подстраивать селектор каждый раз при создании нового ее варианта.

Структурированные имена классов

При создании компонентов и «тем» оформления, которые строятся на них, некоторые классы используются как модификаторы, некоторые как ограничители компонентов, некоторые как их составные части и т.д.

Отследить связь между btn (компонентом), btn-primary (модификатором), btn-group (компонентом), and btn-group-item (составной частью компонента) непросто, так как назначение классов не лежит на поверхности в их именах. Нет единой схемы.

В течение прошлого года я экспериментировал с системами именования, которые помогали бы мне быстрее понимать связи между узлами DOM-фрагмента в плане их оформления, а не пытаться собрать в кучу всю архитектуру сайта, «прыгая» туда-сюда между HTML-, CSS— и JS-файлами. Система сложилась преимущественно под влиянием подхода к именованию из методологии БЭМ, но я привел ее в более читаемую (на мой взгляд) форму.

t-template-name
t-template-name--modifier-name
t-template-name__sub-object
t-template-name__sub-object--modifier-name

component-name
component-name--modifier-name
component-name__sub-object
component-name__sub-object--modifier-name

is-state-type

js-action-name

Я рассматриваю некторые структуры как абстрактные шаблоны, а другие как более автономные компоненты (которые обычно строятся на основе шаблонов). Но такое разделение нужно не всегда.

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

Немного о «сыром» размере файла и HTTP-сжатии

В любой дискуссии о модульном/масштабируемом/объектно-ориентированном CSS высказываются опасения насчет размера файла и его «раздутости». Николь Салливэн часто отмечает в своих выступлениях, что компании вроде Facebook обнаруживают, что размеры файлов только уменьшились (наряду с упрощением поддержки), когда вводят у себя такой подход. Более того, я хотел бы поделиться несколькими забавными случаями из моей практики в связи с эффектом HTTP-сжатия при выводе результата работы препроцессора с кучей HTML-классов.

Когда впервые появился Twitter Bootstrap, я переписал получившийся на выходе CSS, чтобы сравнить с тем, как я писал бы его вручную, и насколько различаются при этом размеры файла. После убирания пробелов вручную написанный CSS-файл был примерно на 10% меньше, чем вывод препроцессора. Однако после gzip-сжатия обоих файлов, результат работы препроцессора оказался примерно на 5% меньше, чем CSS «ручной работы».

Это подчеркивает, как важно сравнивать размеры файлов после HTTP-сжатия, поскольку просто размеры файлов с вырезанными пробелами не показывают всей картины. Это говорит о том, что опытным CSS-разработчикам не нужно слишком опасаться определенного количества повторов в итоговом CSS, потому что HTTP-сжатие прекрасно устраняет их и итоговый размер сжатого файла оказывается даже меньше. Преимущества более удобного в поддержке кода после препроцессоров должны развеять опасения по поводу «красоты» или размера «сырого» CSS-файла.

В другом эксперименте я убрал все атрибуты class из 60-килобайтного HTML-файла, взятого с реального сайта (уже использовавшего множество многоразовых компонентов). Это уменьшило размер файла до 25 килобайт. Но после gzip-сжатия исходного и «вычищенного» файлов их размеры составляли 7,6 и 6 килобайт соответственно — всего 1,6 килобайта разницы. Фактическая значимость вольного использования классов редко стоит того, чтоб на ней зацикливаться.

Как я перестал беспокоиться…

Опыт многих грамотных разработчиков, после многих лет работы, привел к пересмотру того, как надо разрабатывать крупные сайты и веб-приложения. Но несмотря на это, людям, воспитанным идеологией, в которой «семантичный HTML» означает основанные на контенте имена классов (и то как крайняя вынужденная мера), требуется самим поработать над крупным приложением, прежде чем остро почувствовать на себе, насколько непрактичен такой идеализированный подход. Будьте готовы отбрасывать старые идеи, рассматривать альтернативы и даже снова обращаться к подходам, которые вы, возможно, ранее для себя полностью исключили.

Как только вы начнете писать нетривиальные сайты и веб-приложения, которые вам и другим приходится не только поддерживать, но и развивать, вы быстро поймете (о чем Николь Салливэн говорила годами) что, несмотря на все усилия, поддерживать ваш код будет всё труднее и труднее. И стоит потратить время на изучение работ тех людей, кто уже сталкивался с этой проблемой: блога Николь и проекта «Объектно-ориентированный CSS», масштабируемой модульной архитектуры CSS Джонатана Снука (SMACSS) и методологии «Блок, Элемент, Модификатор», разработанной командой «Яндекса»

Как только вы начнете создавать HTML и CSS, то для того, чтобы тратить меньше времени на написание и редактирование CSS, придется смириться с необходимостью тратить больше времени на изменение HTML-классов у элементов, если нужно изменить их стиль. И это оказыватся одинаково практично для разработчиков как клиентской, так и серверной части — каждый может легко переставлять готовые «кирпичики Lego»; оказывается, никто не может творить алхимию в CSS.

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

5 комментариев

  1. Все очень грустно, скажу я вам. «Язык», на котором написана статья, в большинстве случаев, способен понять лишь тот, кто уже владеет данной информацией.
    Все что тут написано, можно написать простым языком и в несколько раз короче и понятнее.
    А если подытожить, то спасибо за статью. Нашел для себя некоторые интересные моменты.

    1. Рад, что хоть что-то полезное в статье нашлось! И прошу учесть, что это всё-таки перевод, я старался не отходить от оригинала. Материал рассчитан на тех, кто, как минимум, в курсе обсуждаемых проблем (уже сталкивался или вот-вот столкнется). Замечания по конкретным косякам перевода и предложения по замене оборотов «сложного языка» простым и понятным с радостью приму на selenit (гав) mail.by :)

  2. Статья неплохая, но действительно не для новичков. Порекомендовал бы читать ее после вводной статьи про методологию БЭМ. Фактически это адаптация методологии для малых сайтов, без задротства со сборкой страницы из отдельных блоков. Также советую глянуть сорцы Twitter Bootstrap, который вполне неплохо спроектирован. Не понравился совет использовать классы с префиксом js- для последующей привязки из JavaScript. Гораздо лучше использовать для этого data-*-атрибуты, которые дадут гибкость и оставят классы для CSS.

    1. Не понравился совет использовать классы с префиксом js- для последующей привязки из JavaScript. Гораздо лучше использовать для этого data-*-атрибуты, которые дадут гибкость и оставят классы для CSS.

      Почему использовать data-атрибуты гораздо лучше? Какую гибкость они дают? В чём она выражается? Зачем оставлять классы только для CSS, ведь есть же уговор, что на классы js-* стили не вешать.

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

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

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