Frontender Magazine

Распределение задач между CSS и JavaScript

Популярность JavaScript невероятна, что подтверждается широким использованием jQuery, Prototype, Node.js, Backbone.js, Mustache и тысяч других библиотек. Этот язык настолько распространен, что его часто используют даже там, где другое решение было бы более удачным в долгосрочной перспективе.

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

Не смешивайте CSS и JavaScript

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

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

Тем не менее, по мере усложнения используемого JavaScript-кода и разработки приложений со сложными интерактивными элементами, становится все труднее не только разделять HTML и JavaScript, но также удержаться от включения стилей непосредственно в документ. Конечно, нельзя однозначно ответить на вопрос стоит ли управлять внешним видом документа с помощью JavaScript. Во многих случаях может возникнуть необходимость добавлять стили динамически, например, в drag-and-drop интерфейсах, где позиционирование элемента может постоянно меняться в зависимости от положения курсора или пальцев пользователя.

Но, вообще говоря, вы можете удобно хранить всю информацию о стилях документа в CSS-файлах и подключать нужные стили через переиспользуемые классы. Такое решение является более гибким, чем добавление стилей с помощью JavaScript, и его можно соотнести с подходом, предполагающим разделение презентационной информации и HTML. Легко следовать этой модели когда имеешь дело только с HTML и CSS, но когда в дело вступает JavaScript – всё начинает развалиться на части. Эта проблема определенно заслуживает нашего внимания.

Множество фронт-энд разработчиков гордится тем, что у них «чистый» HTML. С ним легко работать, а некоторые энтузиасты считают написание такого кода своего рода искусством. Чистый статичный HTML – это прекрасно, но какой в нём смысл, если в итоге сгенерированный HTML-код страницы испещрён динамически добавленными стилями и кусками несемантической разметки? Под «сгенерированным HTML-кодом» я подразумеваю то, что мы получаем после того как исходный HTML будет обработан плагинами и прочим JavaScript. Если первым шагом к отделению оформления от разметки и получению чистого HTML, согласно подходу прогрессивного улучшения, можно считать отказ от атрибута style, то в качестве второго шага я бы предложил отказ от использования JavaScript, который добавляет атрибут style в разметку документа.

Очищаем HTML-код

Я думаю, все согласятся, что слепое использование технологий, как правило, плохая идея. Но довольно часто применяя Jquery мы пользуемся широкими возможностями библиотеки без полного понимания как всё это работает под капотом. В качестве примера того, как сложно на самом деле избежать смешивания JavaScript и CSS, можно привести поведение jQuery-метода hide(). Следуя принципам прогрессивного улучшения, вы вряд ли будете использовать инлайновые стили таким образом:

<div class="content-area" style="display:none;"></div>

Мы не используем такой код, потому что скринридеры игнорируют элементы для которых задано display:none, к тому же инлайновые стили замусоривают HTML-код ненужными презентационными данными. Когда вы используете hide(), он делает ровно то же самое: добавляет заданному элементу атрибут style и указывает для свойства display значение none. hide() легко использовать, но он плохо влияет на доступность содержимого страницы. Мы также нарушаем принципы прогрессивного улучшения, когда добавляем инлайн-стили в HTML-код. Метод hide() часто применяется в интерфейсах со вкладками чтобы скрыть содержимое вкладок, в итоге скринридеры не видят это содержимое вообще. Когда мы приходим к выводу, что добавление стилей с помощью JavaScript, в большинстве случаев, не лучший вариант, мы можем перенести их в CSS и ссылаться на них с помощью класса:

CSS

.hide {
display: none;
}

jQuery

$('.content-area').addClass('hide');

Использование display:none для скрытия содержимого всё ещё создает проблему с доступностью, но, так как мы больше не используем готовый jQuery-метод, мы можем легко управлять тем, каким именно способом должно быть скрыто содержимое. Например, можно использовать такой код:

CSS

.hide {
position: absolute;
top: -9999px;
left: -9999px;
}

.remove {
display: none;
}

В приведенном примере вы можете видеть, что, хотя применение любого из двух классов скроет контент из области видимости, они совершенно по-разному действуют с точки зрения доступности. Также при взгляде на такой код нам сразу ясно, что мы имеем дело со стилями, которые должны находиться в CSS-файле. Универсальные переиспользуемые классы позволяют уменьшить JavaScript-код, и, кроме того, они соотносятся с объектно-ориентированным подходом к написанию CSS (OOCSS). Это действительно следование принципу DRY (Don’t Repeat Yourself) в CSS, который также позволяет в рамках всего проекта использовать более целостный подход к фронт-энд разработке. Лично для себя я вижу много преимуществ в возможности контролировать поведение элементов таким образом, хотя некоторые могли бы назвать это чрезмерной увлеченностью контролем.

Особенности применения в веб-среде и в команде веб-разработчиков

Таким образом, мы можем использовать сильные стороны CSS и JavaScript не теряя равновесия. Создание баланса во фронт-энд разработке крайне важно, потому что веб-окружение очень хрупко, и мы не можем контролировать его так же легко как серверную часть. Если у пользователя установлен старый и медленный браузер, в большинстве случаев, мы не можем это исправить (оффтоп: правда, свою бабушку я уже приучил к Google Crome), мы можем лишь воображать себе тот хаос веб-окружений, в который попадут наши страницы, делать лучшее из возможного и планировать решения на случай худших сценариев.

Со мной часто спорят, мол, добавлять CSS-классы с помощью JavaScript неудобно, когда над проектом работает несколько разработчиков, так как CSS обычно готов до того, как начинается написание JavaScript, и классы, которые были созданы для использования в JavaScript, могут потеряться в общей массе кода и в итоге оказаться продублированными в нескольких местах. В таких случаях я советую постучать себя по лбу, открыть AIM, GTalk или Skype и сообщить коллегам о классах, которые были созданы специально для использования в JavaScript. Я понимаю, что кому-то общение, происходящее не в Git-коммитах, может показаться странным, но всё будет в порядке, я гарантирую это.

Использование динамического CSS с подстраховкой в Javascript

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

В этом примере мы будем использовать: * jQuery * Modernizr

Для начала создадим элемент body и его содержимое:

<body>
  <button type="button">Run Transition</button>
  <div id="cube"></div><!--/#cube-->
</body>

Теперь пропишем CSS:

#cube {
   height: 200px;
   width: 200px;
   background: orange;
   -webkit-transition: opacity linear .5s;
      -moz-transition: opacity linear .5s;
        -o-transition: opacity linear .5s;
           transition: opacity linear .5s;
}

.fade-out {
    opacity: 0;
}

Перед добавлением JavaScript давайте ненадолго отвлечемся, чтобы разобраться с тем, что мы собираемся сделать:

  1. Используем библиотеку Modernizr, чтобы проверить поддерживается ли CSS-свойство transition.
  2. Если оно подерживается:

    2.1. Присваиваем кнопке событие onclick, которое при нажатии на кнопку будет добавлять класс fade-out к элементу с идентификатором #cube.

    2.2. Добавляем слушатель события, который определит завершение transition. Это позволит нам вызвать функцию, которая удалит элемент #cube из DOM.

  3. Если transition не поддерживается:

    3.1. Присваиваем кнопке событие onclick, запускающее jQuery-метод
    animate(), чтобы обеспечить плавное исчезновение элемента #cube.

    3.2. Используем callback-функцию, чтобы удалить #cube из DOM-дерева.

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

Первым делом нужно прописать переменные в JavaScript:

(function () {

    // прописываем переменные
    var elem = document.getElementById('cube'),
       button = document.getElementById('do-it'),
       transitionTimingFunction = 'linear',
       transitionDuration = 500,
       transitionend;

    // прописываем свойство transitionend, используя вендорные префиксы
    if ($.browser.webkit) {
       transitionend = 'webkitTransitionEnd'; // safari & chrome
    } else if ($.browser.mozilla) {
       transitionend = 'transitionend'; // firefox
    } else if ($.browser.opera) {
       transitionend = 'oTransitionEnd'; // opera
    } else {
       transitionend = 'transitionend'; // best guess at the default?
    }

    //... добавляем остальной код сюда.

})(); // конец обёртывающей функции

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

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

// с помощью Modernizr проверяем наличие поддержки transition
if(Modernizr.csstransitions) {

// добавление класса при клике мышкой
   $(button).on('click', function () {
     $(elem).addClass('fade-out');
   });

   // имитация callback-функции при помощи слушателя события
   elem.addEventListener(transitionend, function () {
     theCallbackFunction(elem);
   }, false);

} else {

   // слушатель события для браузеров, не поддерживающих предыдущий вариант 
   $(button).on('click', function () {

     $(elem).animate({
       'opacity' : '0'
     }, transitionDuration, transitionTimingFunction, function () {
       theCallbackFunction(elem);
     });

   }); // конец события

} // конец проверки 

И наконец, нам нужно объявить функцию, общую для обоих процессов, которая будет выполняться после завершения transition (или animation). Чтобы не усложнять наш пример, назовем её просто theCallbackFunction() (хотя технически она не является callback-функцией). Она удалит элемент из DOM-дерева и выведет в консоль сообщение о завершении процесса.

// прописываем callback-функцию, которая выполняется после завершения transition/animation
function theCallbackFunction (elem) {

    'use strict';

    // удаление элемента из DOM
    $(elem).remove();

    // вывод сообщения об успешном завершении
    console.log('the transition is complete');

}

Вне зависимости от браузера, от IE 7 (в худшем случае) до мобильной версии Safari или Chrome, действие кода будет идентичным. Разница скрыта «под капотом» и совершенно незаметна для пользователя. Подобным образом можно применять и другие новомодные техники, не жертвуя при этом удобством пользователей старых браузеров. Этот подход также позволяет хранить CSS отдельно от JavaScript, что, на самом деле, и было нашей целью всё это время.

Мораль истории

Возможно, вы спрашиваете себя: зачем усложнять себе жизнь дополнительной работой? Мы написали 60 строчек JavaScript, чтобы получить эффект, на который нужно всего 8 строчек jQuery. Ну, никто и не говорил, что чистый код и следование принципам прогрессивного улучшения будет лёгкой задачей. Напротив, гораздо проще всё это игнорировать. Однако ответственные разработчики должны следить за тем, чтобы разработанные ими приложения были доступны максимально широкому кругу пользователей и легко масштабировались в будущем. Если вы, также как и я, хотите сделать пойти немного дальше и сделать использование сайта или приложения одинаково удобным для всех, тогда определённо стоит потратить несколько больше времени, чтобы уделить внимание мелочам и создать взаимодействие с пользователем, следуя принципам постепенной деградации и прогрессивного улучшения

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

На действующих проектах мы уже используем переходы и CSS-анимации для создания мелких эффектов вроде изменения по наведению, вращающейся графики или пульсирующих обьектов. Мы постепенно приходим к тому, что CSS становится достаточно мощным языком, который очень хорошо поддерживается браузерами, и всё более распространено использование его для больших анимаций, которые раньше делались с помощью JavaScript. Если вы ищете легкий и надежный инструмент, который относительно просто поддерживать, при том, что он позволяет использовать самые последние и самые мощные возможности браузеров, значит пришло время пересмотреть свои подходы и начать использовать всю мощь сочетания CSS и JavaScript. Как однажды сказал один умный человек, «чтобы писать хороший JavaScript, нужно знать, когда вместо него стоит использовать CSS». (Этот человек - я).

Автор: Тим Райт (Tim Wright)

Если вы заметили ошибку, вы всегда можете отредактировать статью, создать issue или просто написать об этом Антону Немцеву в skype ravencry.

Tim Wright
Aвтор:
Tim Wright
Сaйт:
http://www.csskarma.com/
Twitter:
@csskarma
Наталья Фадеева
Переводчик:
Наталья Фадеева
вКонтакте:
natatik_l
Twitter:
@very_busy_girl
GitHub:
NatalieF

Комментарии (12 комментариев, если быть точным)

Автар пользователя
vvscode

В таких случаях я советую постучать себя по лбу, открыть AIM, GTalk или Skype и сообщить коллегам о классах, которые были созданы специально для использования в JavaScript.

Префикс 'js-' для класса и отдельный файл частично решают эту проблему

Автар пользователя
vdv73rus

$.browser was removed in jQuery 1.9

Автар пользователя
gruzzilkin

Статья 2012 года. Отсюда, видимо, то странное ощущение, что в тексте чего-то не хватает про BEM, React, Angular etc.

Автар пользователя
vyushin

Знать и делать — разные вещи. Все знают, что использовать inline CSS вроде бы не стоит, но всё равно встречаются те, кто используют. Если открыть парочку недавно созданных сайтов, то наверняка там можно будет встретить множество inline CSS.

Я бы ещё акцентировал внимание на том, что HTML, забитый inline CSS, сложнее дебажить. Одно дело, когда к тегу добавляется только display: block; и совсем другое дело, когда надо динамически применить к тегу 5-10 CSS свойств.

Бывает ещё круче. Например, по одному событию к тегу добавляется 3 CSS свойства, а по другому 5 CSS свойств и если всё это добавляется через inline CSS, а не классами, то при дебаге становится очень "весело" (в кавычках).

Статья вполне себе актуальна на сегоднящний день.

Автар пользователя
likerRr

Часто бывает, что применять к тегу 5-10 динамических свойств является необходимость. Например, когда вы используете плагин стороннего разработчика, АПИ которого позволяет менять стили. И даже, если бы вы реализовывали похожий функционал плагина без использования инлайна, то вам придется его использовать, т.к. писать свой велосипед будет дорого. В таком случае я считаю нормой наличие инлайна, либо есть альтернативы?

Автар пользователя
vyushin

@ikerRr, в этом случае согласен. Переписывать дороже выйдет. Это уже вопрос к разработчиком стороннего API, почему они сделали так, а не иначе. :)

Автар пользователя
oskolsky

А как же функции slideUp() и slideDown()? Их свойства невозможно перенести в css.

Или как установить вычисляемые width и hight используя просто классы?

В целом статья занятная, но стоит помнить, что отказаться полностью от inline css не выйдет, главное держать все в балансе и не переборщить.

Автар пользователя
andrewboot

@oskolsky, если я вас правильно понял, то полагаю, свойства transform:translate и transition из CSS3 с этим справятся. Для IE, конечно, придется придумывать отдельные велосипеды.

Автар пользователя
oskolsky

@andreoff-biz transform:translate с этим не справится, так как в данном случае придется менять высоту элемента. Помогло бы изменение height и transition, но это не работает там, где высота не известна :)

Автар пользователя
AndrewCeM

@oskolsky, в таком случае вам поможет свойство max-height вместо height. Это решит проблему с неизвестной высотой.

Автар пользователя
DimaPopov

Ребята, попробуйте OrnaJS, штука для стилизации SPA и landing page. ornaorg.github.io Free source

Автар пользователя
iamstarkov

@DimaPopov ни тестов, ни документации, height_100pxbgi_url_dolan.jpg=((

— прости, но это неправильно — так грубо рекламировать такой сырой проект