Статьи

Автор: Миндубаев Андрей aka Covex
Опубликована: 25 сентября 2008

Компоненты в Unobtrusive JavaScript

Это продолжение моей статьи «Клиентская оптимизация и этапы разработки». В ней были даны рекомендации по созданию быстрых сайтов, а в том числе, фактически, я рассказал что должен сделать Web-разработчик, чтобы следовать принципам «Ненавязчивого JavaScript»:

  • разделение структуры (HTML) / оформления (CSS) и поведения (JavaScript);
  • использование JavaScript для повышения удобства использования уже рабочего приложения;
  • применение техники Graceful degradation — если браузер не поддерживает те или иные функции, которые мы добавляем в приложение с помощью JavaScript — приложение всё равно остается рабочим.

В этой же статье я хотел бы рассказать об алгоритме реализации принципов «ненавязчивости» на JavaScript.

JavaScript-функционал — производная от HTML и CSS.

Сверстать страницу и реализовать какой-то серверный функционал — относительно просто. Сложно — построить фундамент для работы JS-программистов. Семантическая верстка, при которой каждый структурный HTML-элемент выбирается на основе его предназначения, — это необходимая, но не достаточная основа для такого фундамента.

Функциональное оформление

Эффективный обмен информацией внутри команды — залог высокой скорости и качества разработки. Семантическая верстка не накладывает на HTML-верстальщика требование показать JS-программисту правила отображения страницы в динамике. Это задача функционального оформления, — специального набора CSS-правил, показывающих как должен меняться вид страницы после каких-либо действий пользователя. Приведу пример:

<style type="text/css">

 .contents li { display: block; }
 .v1 .contents li.v1, .v2 .contents li.v2, .v3 .contents li.v3 { display: none; }
 .links li { display: none; }
 .v1 .links li.v1, .v2 .links li.v2, .v3 .links li.v3 { display: block; }
</style>
<div class="v1 v2">
 <ol class="contents">

  <li class="v1">content1</li>
  <li class="v2">content2</li>
  <li class="v3">content3</li>

 </ol>
 <ol class="links">
  <li class="v1">link1</li>

  <li class="v2">link2</li>
  <li class="v3">link3</li>

 </ol>
</div>

На экране отобразится 3 строки: content3, link1 и link2. Здесь CSS-правила написаны таким образом, что при добавлении/удалении в класс корневого DIV-а v1, v2 или v3 скроются или покажутся элементы LI списков .contents и .links. Эти CSS-правила и будут «инструкцией» для JS-программиста — теперь он с лёгкостью сможет сделать какую-нибудь функцию switchContentsAndLinks, которая бы «переключала» видимость содержимого DIV-а.

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

Компоненты — программные модули «ненавязчивого» JavaScript

Компонента — это объект JavaScript, связанный с DOM-элементом. Для простоты можно сказать, что Компонента и DOM-элемент — это одно и тоже.

Компоненты могут:

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

В HTML-коде, приведённом выше можно было бы выделить 7 компонент:

  • 1) DIV — может менять свой className по какому-то сигналу;
  • 2-7) LI — может посылать сигнал своей родительской компоненте по клику на себя.

Создание объектов-Компонент

Команда «The Exceptional Performance» из Yahoo разработала набор правил для создания быстрых Web-страниц. Список включает в себя 34 пункта, объединённых в 7 категорий. Принципы Unobtrusive JavaScript не конфликтуют с этими правилами, наоборот — их цели похожи. Поэтому некоторые пункты Алгоритма будут подкреплены ссылками на эти правила:

  1. Для того, чтобы определить какие из DOM-элементов являются Компонентами, необходимо их как-то пометить. А также описать какие файлы нужно загрузить для работы каждого из них. При разработке нашего проекта мы решили сделать «пометку» внутри className DOM-элемента: он строится из трёх частей через пробел:
    • Первая часть: «js» — признак того, что DOMElement — это компонент;
    • Вторая часть: имя класса компонента. По названию класса можно однозначно определить, где он лежит на сервере;
    • Третья часть, необязательная — это Presentation (оформление) в чистом виде.
  2. Если для работы компонента, нужны дополнительные данные, то мы их передаём в атрибутах DOM-элемента;
  3. Если для работы компонента нужен некий массив данных, то эти данные можно передать в onclick DOM-элемента: onclick="return { data: { }}", при инициализации можно прочитать данные используя конструкцию вида var data = DOMElement.onclick();
  4. Чтобы не мешать загрузке контента и изображений, мы помещаем код загрузки основной JS-библиотеки перед закрытием тэга body;
  5. После загрузки страницы производим выборку элементов CSS-селектором: (1) ищем все элементы, имеющие класс js; (2) из строкового значения className DOM-элемента берём название класса компонента (в идеале — это первый и последний раз, когда мы ищем элементы в документе).
  6. Имея список классов компонент страницы, мы должны загрузить недостающий функционал. Различных классов компонент на странице может быть достаточно много, поэтому для загрузки мы используем метод, описанный в моей прошлой статье (в кратце: для классов компонент aaa_bbb и ccc_ddd, то есть для файлов /jas/aaa/bbb.js и /jas/ccc/ddd.js формируется только один запрос к серверу вида /jas/aaa,bbb.js;ccc,ddd.js/)
  7. После загрузки всего необходимого функционала, мы можем создать объекты-экземпляры классов Компонент.

Классы Компонент

Говоря «класс» я имею ввиду Class в понимании классического ООП. Использование такого подхода было продиктовано необходимостью повторного использования кода: вызовом конструкторов и деструкторов родительских классов после создания объекта и перед его уничтожением. Описание того, как происходит наследование в моих «классах» я выложил в моей статье-приложении «Классы в JavaScript: вызов методов родительского класса».

Возвращаясь к Компонентам, я бы хотел показать иерархию классов, имеющуюся в данный момент:

JooS.Class
|
+- JooS.TextElement
|
+- JooS.Element
   |
   +- Beatle.Class
       |
       +- Любой конечный Класс Компонента

JooS.Class — Абстрактный класс, общий предок.

JooS.Element — Класс «DOM-элемент», содержит свойство htmlElement и методы работы с ним

Beatle.Class — Абстрактный класс «Компонент», содержит методы совместной работы компонент

Также, специально для этой статьи, я подготовил страничку с готовыми компонентами: http://beatle.joos.nnov.ru/. Страница состоит из трёх частей:

  1. Список — содержит компоненты для HTML-кода, приведённый в главе «Функциональное оформление». Используется 3 класса компонент;
  2. «Анимированный» PNG — используется 1 класс
  3. Форма комментария — используется 7 классов.
  4. + ещё 1 класс GoogleAnalytics для body чтобы всех сосчитать (по мотивам статьи Разгоняем счетчики: от мифов к реальности)

О принципах совместной работы нескольких компонент будет рассказано ниже на примере «Формы комментария».

Обмен информацией между компонентами

Совместная работа Компонент подразумевает под собой «обмен информацией». Способ, в котором компоненты зная друг о друге, напрямую вызывают функции соседней компоненты не подходит, т. к. это противоречит третьему принипу Unobtrusive JavaScript — соседней компоненты просто может не быть на странице и вызов вида this.neibourComponent.method1() вызовет ошибку JS. Проверки существования соседей для такого случая так же не подходят — таких проверок может быть очень много + заранее неизвестно сколько компонент захотят знать эту информацию.

С другой стороны необходимость в обмене информацией возникает внутри компоненты только при изменении состояния компоненты, например, после клика на её DOM-элемент. Назовём такое изменение состояния «Событием компонент» (далее — просто «Событие»). Такие События характеризуются идентификатором (именем), входными параметрами и функцией обработки входных параметров (например, для преобразования строки «10px» в число 10).

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

У меня получилось так, что любой компонент может выступать в роли «Бросающего», «Слушателя» и «Диспетчера» одновременно. Это вызвало долгие споры в нашей команде, но в конце концов все согласились, что структура кода HTML не только позволяет, но и диктует такой вариант решения.

Практически всегда «Слушатель» ожидает событие от своего соседнего компонента, и никак не от другого такого-же, но находящегося в другом конце страницы: например, компонента-валидаторФормы может ждать событие «форма заполнена» только от своей дочерней компоненты-FORM, а компонента-FORM ожидает события изменения значения только от своих дочерних компонент-INPUT-ов. Таким образом, возникает необходимость в нескольких диспетчерах событий + для работы системы без сбоев должен существовать хотябы один диспетчер «по умолчанию».

Побочным эффектом такой системы стала возможность «бросать событие в себя» или «слушать событие у себя» и необходимость регистрироваться у «родительской-компоненты-чёткоОпределённогоКласса» или у «строго родительской-компоненты».

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

  • Функция «Слушателя» события бросает Exception ( throw { /* объект_сИнформацией */ }; }
  • объект_сИнформацией будет доставлен в качестве результата функции броситьСобытиеКомпоненты('имя', параметры);

Пример: компонент-FORM бросает валидатору событие «форма заполнена»; валидатор в случае ошибки возвращает объект-описание-ошибки.

Заключение

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

Говорят, что JavaScript замедляет Web. Я же этой и предыдущей статьёй хотел показать, что всё в наших руках: мы можем сделать Web быстрее! (ну, или хотябы, создать видимость =)

Спасибо за внимание.

Читать дальше

Все комментарии (habrahabr.ru)