Это продолжение моей статьи «Клиентская оптимизация и этапы разработки». В ней были даны рекомендации по созданию быстрых сайтов, а в том числе, фактически, я рассказал что должен сделать Web-разработчик, чтобы следовать принципам «Ненавязчивого JavaScript»:
Сверстать страницу и реализовать какой-то серверный функционал — относительно просто. Сложно — построить фундамент для работы JS-программистов. Семантическая верстка, при которой каждый структурный HTML-элемент выбирается на основе его предназначения, — это необходимая, но не достаточная основа для такого фундамента.
Эффективный обмен информацией внутри команды — залог высокой скорости и качества разработки. Семантическая верстка не накладывает на HTML-верстальщика требование показать JS-программисту правила отображения страницы в динамике. Это задача функционального оформления, — специального набора CSS-правил, показывающих как должен меняться вид страницы после каких-либо действий пользователя. Приведу пример:
Таким образом, семантичность вёрстки и функциональность оформления являются связующими звеньями между структурой, оформлением и поведением документа и прочным фундаментом для разработки функционала на JavaScript.
Компонента — это объект JavaScript, связанный с DOM-элементом. Для простоты можно сказать, что Компонента и DOM-элемент — это одно и тоже.
Компоненты могут:
Команда «The Exceptional Performance» из Yahoo разработала набор правил для создания быстрых Web-страниц. Список включает в себя 34 пункта, объединённых в 7 категорий. Принципы Unobtrusive JavaScript не конфликтуют с этими правилами, наоборот — их цели похожи. Поэтому некоторые пункты Алгоритма будут подкреплены ссылками на эти правила:
Говоря «класс» я имею ввиду Class в понимании классического ООП. Использование такого подхода было продиктовано необходимостью повторного использования кода: вызовом конструкторов и деструкторов родительских классов после создания объекта и перед его уничтожением. Описание того, как происходит наследование в моих «классах» я выложил в моей статье-приложении «Классы в JavaScript: вызов методов родительского класса».
Возвращаясь к Компонентам, я бы хотел показать иерархию классов, имеющуюся в данный момент:
Совместная работа Компонент подразумевает под собой «обмен информацией». Способ, в котором компоненты зная друг о друге, напрямую вызывают функции соседней компоненты не подходит, т.к. это противоречит третьему принипу Unobtrusive JavaScript — соседней компоненты просто может не быть на странице и вызов вида this.neibourComponent.method1() вызовет ошибку JS. Проверки существования соседей для такого случая так же не подходят — таких проверок может быть очень много + заранее неизвестно сколько компонент захотят знать эту информацию.
С другой стороны необходимость в обмене информацией возникает внутри компоненты только при изменении состояния компоненты, например, после клика на её DOM-элемент. Назовём такое изменение состояния «Событием компонент» (далее — просто «Событие»). Такие События характеризуются идентификатором (именем), входными параметрами и функцией обработки входных параметров (например, для преобразования строки «10px» в число 10).
Назовём компоненту, генерирующую событие, «Бросающим», а компоненту, подписанную на это событие, — «Слушателем». Назовём «Диспетчером» компонент, через который «Бросающий» передаёт событие «Слушателям». Для того, чтобы передать событие, «Бросающий» и «Слушатель» должны сначала зарегистрироваться у «родительского-компонента-диспетчера» и только после этого начать передачу. Такая регистрация происходит в конструкторе компонента.
У меня получилось так, что любой компонент может выступать в роли «Бросающего», «Слушателя» и «Диспетчера» одновременно. Это вызвало долгие споры в нашей команде, но в конце концов все согласились, что структура кода HTML не только позволяет, но и диктует такой вариант решения.
Практически всегда «Слушатель» ожидает событие от своего соседнего компонента, и никак не от другого такого-же, но находящегося в другом конце страницы: например, компонента-валидаторФормы может ждать событие «форма заполнена» только от своей дочерней компоненты-FORM, а компонента-FORM ожидает события изменения значения только от своих дочерних компонент-INPUT-ов. Таким образом, возникает необходимость в нескольких диспетчерах событий + для работы системы без сбоев должен существовать хотябы один диспетчер «по умолчанию».
Побочным эффектом такой системы стала возможность «бросать событие в себя» или «слушать событие у себя» и необходимость регистрироваться у «родительской-компоненты-чёткоОпределённогоКласса» или у «строго родительской-компоненты».
Кроме случая, когда инициатором события является «источник информации», существует вариант, когда в роли инициатора может выступать «потребитель информации». Это решается так же, через механизм событий:
Вот такой вот алгоритм. Быть может с виду он покажется вам сложным, но на самом деле имея подобные инструменты у себя под рукой можно быстро создавать довольно сложные вещи. Это один из редких случаев, когда, казалось бы, искусственные ограничения и сложности («семантическая вёрстка» и «функциональное оформление») дают большой прирост в производительности команды и результата её работы.
Говорят, что JavaScript замедляет Web… Я же этой и предыдущей статьёй хотел показать, что всё в наших руках: мы можем сделать Web быстрее! (ну, или хотябы, создать видимость =)
Спасибо за внимание.
В этой же статье я хотел бы рассказать об алгоритме реализации принципов «ненавязчивости» на JavaScript.
- разделение структуры (HTML) / оформления (CSS) и поведения (JavaScript);
- использование JavaScript для повышения удобства использования уже рабочего приложения;
- применение техники Graceful degradation — если браузер не поддерживает те или иные функции, которые мы добавляем в приложение с помощью JavaScript — приложение всё равно остается рабочим.
JavaScript-функционал — производная от HTML и CSS.
Сверстать страницу и реализовать какой-то серверный функционал — относительно просто. Сложно — построить фундамент для работы JS-программистов. Семантическая верстка, при которой каждый структурный HTML-элемент выбирается на основе его предназначения, — это необходимая, но не достаточная основа для такого фундамента.
Функциональное оформление
Эффективный обмен информацией внутри команды — залог высокой скорости и качества разработки. Семантическая верстка не накладывает на HTML-верстальщика требование показать JS-программисту правила отображения страницы в динамике. Это задача функционального оформления, — специального набора CSS-правил, показывающих как должен меняться вид страницы после каких-либо действий пользователя. Приведу пример:
<style type="text/css">На экране отобразится 3 строки: content3, link1 и link2. Здесь CSS-правила написаны таким образом, что при добавлении/удалении в класс корневого DIV-а v1, v2 или v3 скроются или покажутся элементы LI списков .contents и .links. Эти CSS-правила и будут «инструкцией» для JS-программиста — теперь он с лёгкостью сможет сделать какую-нибудь функцию switchContentsAndLinks, которая бы «переключала» видимость содержимого DIV-а.
.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>
Таким образом, семантичность вёрстки и функциональность оформления являются связующими звеньями между структурой, оформлением и поведением документа и прочным фундаментом для разработки функционала на JavaScript.
Компоненты — программные модули «ненавязчивого» JavaScript
Компонента — это объект JavaScript, связанный с DOM-элементом. Для простоты можно сказать, что Компонента и DOM-элемент — это одно и тоже.
Компоненты могут:
- содержать в себе функции-обработчики событий DOM-элемента;
- менять состоятие своего DOM-элемента;
- содержать дочерние компоненты, быть дочерней компонентой;
- обмениваться информацией с другими компонентами.
- 1) DIV — может менять свой className по какому-то сигналу;
- 2-7) LI — может посылать сигнал своей родительской компоненте по клику на себя.
Создание объектов-Компонент
Команда «The Exceptional Performance» из Yahoo разработала набор правил для создания быстрых Web-страниц. Список включает в себя 34 пункта, объединённых в 7 категорий. Принципы Unobtrusive JavaScript не конфликтуют с этими правилами, наоборот — их цели похожи. Поэтому некоторые пункты Алгоритма будут подкреплены ссылками на эти правила:
- Для того, чтобы определить какие из DOM-элементов являются Компонентами, необходимо их как-то пометить. А также описать какие файлы нужно загрузить для работы каждого из них. При разработке нашего проекта мы решили сделать «пометку» внутри className DOM-элемента: он строится из трёх частей через пробел:
- Первая часть: «js» — признак того, что DOMElement — это компонент;
- Вторая часть: имя класса компонента. По названию класса можно однозначно определить, где он лежит на сервере;
- Третья часть, необязательная — это Presentation (оформление) в чистом виде.
- Если для работы компонента, нужны дополнительные данные, то мы их передаём в атрибутах DOM-элемента;
- Если для работы компонента нужен некий массив данных, то эти данные можно передать в onclick DOM-элемента: onclick="return { data: {… }}", при инициализации можно прочитать данные используя конструкцию вида var data = DOMElement.onclick();
- Чтобы не мешать загрузке контента и изображений, мы помещаем код загрузки основной JS-библиотеки перед закрытием тэга body;
- После загрузки страницы производим выборку элементов CSS-селектором: (1) ищем все элементы, имеющие класс js; (2) из строкового значения className DOM-элемента берём название класса компонента (в идеале — это первый и последний раз, когда мы ищем элементы в документе).
- Имея список классов компонент страницы, мы должны загрузить недостающий функционал. Различных классов компонент на странице может быть достаточно много, поэтому для загрузки мы используем метод, описанный в моей прошлой статье (в кратце: для классов компонент aaa_bbb и ccc_ddd, то есть для файлов /jas/aaa/bbb.js и /jas/ccc/ddd.js формируется только один запрос к серверу вида /jas/aaa,bbb.js;ccc,ddd.js/)
- После загрузки всего необходимого функционала, мы можем создать объекты-экземпляры классов Компонент.
Классы Компонент
Говоря «класс» я имею ввиду Class в понимании классического ООП. Использование такого подхода было продиктовано необходимостью повторного использования кода: вызовом конструкторов и деструкторов родительских классов после создания объекта и перед его уничтожением. Описание того, как происходит наследование в моих «классах» я выложил в моей статье-приложении «Классы в JavaScript: вызов методов родительского класса».
Возвращаясь к Компонентам, я бы хотел показать иерархию классов, имеющуюся в данный момент:
Также, специально для этой статьи, я подготовил страничку с готовыми компонентами: http://beatle.joos.nnov.ru/. Страница состоит из трёх частей:JooS.Class | +- JooS.TextElement | +- JooS.Element | +- Beatle.Class | +- Любой конечный Класс Компонента
JooS.Class — Абстрактный класс, общий предок.
JooS.Element — Класс «DOM-элемент», содержит свойство htmlElement и методы работы с ним
Beatle.Class — Абстрактный класс «Компонент», содержит методы совместной работы компонент
- Список — содержит компоненты для HTML-кода, приведённый в главе «Функциональное оформление». Используется 3 класса компонент;
- «Анимированный» PNG — используется 1 класс
- Форма комментария — используется 7 классов.
- + ещё 1 класс GoogleAnalytics для body чтобы всех сосчитать (по мотивам статьи Разгоняем счетчики: от мифов к реальности)
Обмен информацией между компонентами
Совместная работа Компонент подразумевает под собой «обмен информацией». Способ, в котором компоненты зная друг о друге, напрямую вызывают функции соседней компоненты не подходит, т.к. это противоречит третьему принипу Unobtrusive JavaScript — соседней компоненты просто может не быть на странице и вызов вида this.neibourComponent.method1() вызовет ошибку JS. Проверки существования соседей для такого случая так же не подходят — таких проверок может быть очень много + заранее неизвестно сколько компонент захотят знать эту информацию.
С другой стороны необходимость в обмене информацией возникает внутри компоненты только при изменении состояния компоненты, например, после клика на её DOM-элемент. Назовём такое изменение состояния «Событием компонент» (далее — просто «Событие»). Такие События характеризуются идентификатором (именем), входными параметрами и функцией обработки входных параметров (например, для преобразования строки «10px» в число 10).
Назовём компоненту, генерирующую событие, «Бросающим», а компоненту, подписанную на это событие, — «Слушателем». Назовём «Диспетчером» компонент, через который «Бросающий» передаёт событие «Слушателям». Для того, чтобы передать событие, «Бросающий» и «Слушатель» должны сначала зарегистрироваться у «родительского-компонента-диспетчера» и только после этого начать передачу. Такая регистрация происходит в конструкторе компонента.
У меня получилось так, что любой компонент может выступать в роли «Бросающего», «Слушателя» и «Диспетчера» одновременно. Это вызвало долгие споры в нашей команде, но в конце концов все согласились, что структура кода HTML не только позволяет, но и диктует такой вариант решения.
Практически всегда «Слушатель» ожидает событие от своего соседнего компонента, и никак не от другого такого-же, но находящегося в другом конце страницы: например, компонента-валидаторФормы может ждать событие «форма заполнена» только от своей дочерней компоненты-FORM, а компонента-FORM ожидает события изменения значения только от своих дочерних компонент-INPUT-ов. Таким образом, возникает необходимость в нескольких диспетчерах событий + для работы системы без сбоев должен существовать хотябы один диспетчер «по умолчанию».
Побочным эффектом такой системы стала возможность «бросать событие в себя» или «слушать событие у себя» и необходимость регистрироваться у «родительской-компоненты-чёткоОпределённогоКласса» или у «строго родительской-компоненты».
Кроме случая, когда инициатором события является «источник информации», существует вариант, когда в роли инициатора может выступать «потребитель информации». Это решается так же, через механизм событий:
- Функция «Слушателя» события бросает Exception ( throw { /* объект_сИнформацией */ }; }
- объект_сИнформацией будет доставлен в качестве результата функции броситьСобытиеКомпоненты('имя', параметры);
Заключение
Вот такой вот алгоритм. Быть может с виду он покажется вам сложным, но на самом деле имея подобные инструменты у себя под рукой можно быстро создавать довольно сложные вещи. Это один из редких случаев, когда, казалось бы, искусственные ограничения и сложности («семантическая вёрстка» и «функциональное оформление») дают большой прирост в производительности команды и результата её работы.
Говорят, что JavaScript замедляет Web… Я же этой и предыдущей статьёй хотел показать, что всё в наших руках: мы можем сделать Web быстрее! (ну, или хотябы, создать видимость =)
Спасибо за внимание.