Распределение задач между 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.

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

Для начала создадим элемент 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)