Использование классов CSS как флагов в jQuery

Дейв Уорд (Dave Ward) | 4 января 2010 г.

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

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

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

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

В сущности, такой подход не особенно оригинален. Вероятно, вам приходилось его видеть в примерах, предоставленных популярными подключаемыми модулями jQuery, такими как мини-приложения пользовательского интерфейса jQuery Datepicker и Tabs. Часто правилом для этих подключаемых модулей является декларативная пометка элементов с помощью класса CSS, а затем использование селектора jQuery после инициализации страницы для поиска этих элементов и применения функциональных возможностей подключаемого модуля. Это работает как удобное, выразительное средство для объявления функциональности во время разработки, но его полезность выходит далеко за пределы объявления.

Классы CSS: уже не только для задания стилей

Прерывистая зеленая линия, которой Visual Studio подчеркивает отсутствующие классы, может выглядеть как отправка отрицательного отзыва, но это преувеличение. Хотя это не сразу становится очевидным, браузеры не проверяют, все ли классы CSS элемента фактически заданы, и даже не пытаются это сделать. Приравнен ли класс, назначенный элементу, допустимому классу CSS или нет, браузеры все равно добавляют строки этого класса CSS свойству элемента className в модели DOM. Более того, браузеры поддерживают эти данные на протяжении существования каждого элемента, даже если классы никогда не приводят к фактическому заданию стилей.

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

Несколько классов — это хорошо

Продолжая в этом направлении, можно отметить следующий мало используемый аспект CSS: каждый элемент не ограничен только одним классом. Одному элементу можно назначить несколько классов, просто разделяя эти классы пробелами. В качестве примера можно привести следующий элемент параграфа:

<p class="productDescription newProduct">Lorem Ipsum</p>

Его представление будет отражать стили от обоих классов productDescription и newProduct. Кроме того, этот элемент будет возвращаться в результате запроса getElementsByClassName для каждого класса. Помимо задания стилей, этот подход можно применять аналогично использованию атрибутов expando:

<p productDescription="true" newProduct="true">Lorem Ipsum</p>

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

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

Именование согласно функции, а не представлению

При использовании классов CSS в качестве флагов существует такая замечательная особенность, что при этом стимулируется выполнение рекомендаций при именовании самих классов. То есть приходится всегда использовать имена классов, которые описывают, что это за элемент, а не то, как должно выглядеть его представление. Например, если применяется мини-приложение Tabs jQueryUI, то естественным образом возникает желание пометить элемент классом .tabs, чтобы описать его поведение, вместо того чтобы использовать класс с мало говорящим именем, описывающим конкретный стиль, например .blueRounded или .orangeGradient.

Использование преимуществ собственной скорости браузера

Теперь, когда каждый современный браузер реализует метод getElementsByClassName, выполнение запросов элементов на основе имен их классов CSS является быстрой задачей самого браузера. Независимо от того, насколько оптимизирована библиотека JavaScript, интерпретированный код JavaScript заметно медленнее, чем методы API браузера, реализованные в собственном коде.

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

К сожалению, метод getElementsByClassName является относительно новым, и недоступен в некоторых еще используемых старых браузерах, таких как Internet Explorer 7 и Firefox 2. Это единственная область, где прекрасно подходит селекторный обработчик Sizzle jQuery. В браузерах, которые поддерживают метод getElementsByClassName, jQuery уступает по максимальной производительности собственному методу браузера. Однако он также предоставляет собственную реализацию JavaScript с теми же функциональными возможностями для использования в старых браузерах, в которых отсутствует встроенный метод getElementsByClassName.

Пример. Обслуживание массива состояний по сравнению с использованием классов CSS как флагов

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

Сначала предположим, что имеется неупорядоченный список элементов, такой как представленный ниже.

<ul>
  <li class="selected">Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>

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

Использование массива отслеживания

Селекторный обработчик jQuery выполняет быструю работу по выбору элементов <li>, а также предоставляет простой способ подключения обработчика события щелчка к каждому из них. Однако обслуживание массива для отслеживания состояния даже в таком простом сценарии создает путаницу:

// Глобальный массив для отслеживания состояния выбора элементов.
 
var selectedItems = [];
 
 
 
$(document).ready(function () {
 
  // Проход по элементам <li> и инициализация
 
  // глобального массива с начальным состоянием выбора элементов.
 
  $('li').each(function (i, item) {
 
    // Если элемент помечен классом "selected",
 
    // его состояние инициализируется как true.
 
    selectedItems[i] = $(item).hasClass('selected');
 
  });
 
 
 
  // При щелчке элемента его отображение переключается и обновляется
 
  // его состояние в массиве selectedItems.
 
  $('li').click(function () {
 
    // Поиск индекса <li>, вызвавшего событие
 
    // щелчка, соответствующего массиву selectedItems.
 
    var index = $('li').index(this);
 
 
 
    // Инвертирование выбранного состояния данного элемента
 
    // в массиве отслеживания.
 
    selectedItems[index] = !selectedItems[index];
 
 
 
    // Обновление отображения элемента в соответствии
 
    // с состоянием массива отслеживания.
 
    if (selectedItems[index])
 
      $(this).addClass('selected');
 
    else
 
      $(this).removeClass('selected');
 
  });
 
});

[jsbin.com/uwabi/edit]

Как можно видеть, для обслуживания массива состояний требуется довольно много работы. Вы заметили, что уже существует структура данных, обслуживающая те же сведения о состоянии, даже до реализации массива selectedItems?

Использование классов CSS как флагов

Поскольку выбранные элементы всегда помечены классом .selected, внутреннее представление браузера этих элементов — модель DOM — уже обслуживает именно те сведения о состоянии, которые требуется отслеживать. Скорее даже это может быть еще более точно, поскольку отражает именно то, что видит пользователь.

Если реализовать те же функции и полагаться на модель DOM при отслеживании состояний элементов, то код становится существенно проще:

$(document).ready(function () {
  // When an item is clicked, toggle its display, by 
  // setting the .selected class which also serves as
  // a flag for state tracking.
  $('li').click(function () {
    $(this).toggleClass('selected');
  });
});

[jsbin.com/iyaju/edit]

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

Служебные программы jQuery для выбора классов CSS и работы с ними

jQuery предоставляет несколько удобных методов в изолированном наборе, несколько методов обхода и пару фильтров селектора, которые весьма удобны при использовании классов CSS как флагов. Эти средства — преимущества использования идиоматического jQuery вместо того, чтобы заново изобретать колесо. Когда вы действуете в соответствии с общим характером структуры, то структура помогает, а не препятствует.

Синтаксис селектора

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

селектор.имя_класса — при добавлении суффикса .имя_класса к какому-либо селектору выбор сужается только до элементов, помеченных классом CSS с именем имя_класса. Например, следующий селектор выбирает единственный выбранный ("selected") элемент <li> из предыдущего примера:

$('li.selected')

:not(.имя_класса) — как следует из его имени, :not ограничивает выбор, удаляя элементы, соответствующие предоставленному селектору. Он является противоположностью синтаксиса "селектор.имя_класса". Например, можно указать :not, чтобы извлечь противоположное тому, что извлекалось в предыдущем примере:

$('li:not(.selected)')

:has(.имя_класса) — фильтр :has на первый взгляд выглядит неочевидным, ведь можно легко предположить, что его действие аналогично действию синтаксиса селектор.имя_класса. На самом деле :has проверяет наличие указанного селектора в любом элементе, который содержится внутри выбранного элемента, а не в самом выбранном элементе.

Предположим, что требуется найти какие-либо элементы <ul>, содержащие хотя бы один элемент <li> с флагом CSS selected. Фильтр :has предоставляет четкий и удобочитаемый синтаксис для выражения такого запроса:

$('ul:has(.selected)')

Общие методы в изолированном наборе

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

Однако в сочетании с гибким селекторным обработчиком jQuery работа этих методов с несколькими элементами является мощной концепцией. Такой стиль предоставляет средство эффективной работы с группами элементов с минимумом кода.

hasClass — этот метод возвращает логическое значение, указывающее, имеет ли какой-либо элемент в наборе указанный класс CSS. Такая проверка эффективна, даже если предназначена только для одного элемента, и становится гораздо более мощной при использовании ее для набора элементов. Например, в предыдущем примере проверка $('ul li').hasClass('selected') обеспечивает простой и выразительный способ обнаружения, выбраны ли какие-либо элементы, без необходимости прохода по всем элементам списка.

Примечание. hasClass('класс') — это то же самое, что и is('.класс'). В данном примере я использую hasClass, поскольку это более выразительно и удобочитаемо.

addClass(имя_класса) — как следует из его имени, этот метод  применяет класс CSS к набору, в котором он вызван. В контексте данной статьи это метод, который следует использовать для пометки одного или нескольких элементов. Если с помощью addClass добавляется класс в элемент, который уже имеет этот класс, то jQuery достаточно «сообразителен», чтобы не добавлять этот класс повторно. Отсюда следует, что этот метод также можно использовать для обеспечения группы элементов одним и тем же флагом CSS вслепую; это прекрасно подходит для функциональности типа «select all» (выбрать все).

removeClass(имя_класса) — этот метод противоположен методу addClass. Он удаляет указанный класс из любого элемента изолированном наборе jQuery, помеченного этим классом CSS. Как и в случае других методов, выполнение операции removeClass в наборе, содержащем элементы, которые уже не имеют указанного класса, не повлечет за собой ошибок.

toggleClass(имя_класса) — будучи исключительно полезным методом при трактовке классов CSS как флагов, toggleClass инвертирует наличие указанного класса в наборе элементов jQuery, в котором он вызывается. При вызове в разнородном наборе элементов этот метод воспринимает каждый элемент отдельно и переключает его независимо от остальных. Данный метод можно использовать для лаконичного инвертирования указанного флага CSS во всей группе элементов, без необходимости индивидуального прохода по ним.

Методы обхода

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

not(.имя_класса) — этот метод удаляет все элементы, соответствующие указанному селектору. Например, следующий код удаляет элементы из выбранного набора элементов <li>, если они помечены классом .selected:

$('li').not('.selected')

filter(.имя_класса) — метод filter, противоположный методу not, сужает набор только до тех элементов, которые соответствуют предоставленному селектору. Как и в случае метода not, выражение селектора не обязательно относится к CSS, поэтому необходимо добавлять в начале точку, чтобы указать, что селектор является классом CSS. Чтобы выполнить пример, противоположный примеру с методом not, можно выбрать только элементы, помеченные классом .selected, с помощью следующего кода:

$('li').filter('.selected')

Использование jQuery для извлечения данных из модели DOM

Использование модели DOM для отслеживания состояний прекрасно подходит для приложений, полностью работающих на клиенте, но что насчет приложений, которым все же потребуется передавать свое состояние вне модели DOM или даже вне браузера? Вы, конечно, не захотите отправлять модель DOM полностью в вызове AJAX, только чтобы сообщить, какой из трех элементов <li> выбран.

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

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

var selectedItems = $('li').map(function() {
  return $(this).hasClass('selected')
});

При выполнении этого кода создается массив логических значений, указывающих, применяется ли класс CSS .selected к каждому из элементов <li>. В результате становится доступным точно такой же массив, который получался вручную в первом примере кода. Этот массив прекрасно подходит для отправки на сервер в запросе AJAX с сохранением в куки-файле для будущих посещений или для любого количества других вариантов применения, кроме того, не нужно никакое беспорядочное отслеживание вручную, и этот массив будет гарантированно синхронизирован с тем, что видит пользователь во время создания массива.

Почему не метод jQueryData?

Если вы знакомы со встроенным методом jQuery data, то может возникнуть вопрос, почему я выбрал вместо него использование классов CSS в качестве флагов. Метод data несколько похож в том, что его можно использовать для привязки произвольных данных к элементам. Однако он не подходит, если требуется использовать эти данные позднее. Несмотря на то, что для поиска флагов CSS доступен широкий диапазон селекторов, фильтров и методов обхода jQuery, отсутствуют подходящие служебные функции для использования сведений, сохраненных с помощью метода data. Для выполнения аналогичных запросов флагов, установленных с помощью метода data, потребуется проход по всему набору элементов-кандидатов и проверка значений data каждого из этих элементов. Часто это может приводить к проходу по всей модели DOM, что нивелирует преимущество от использования в браузере getElementsByClassName в процессе.

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

Расширяемые возможности задания стилей

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

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

Доверие браузеру — не оправдание

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

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