Это продолжение моей статьи «Клиентская оптимизация и этапы разработки». В ней были даны рекомендации по созданию быстрых сайтов, а в том числе, фактически, я рассказал что должен сделать Web-разработчик, чтобы следовать принципам «Ненавязчивого JavaScript»:
- разделение структуры (HTML) / оформления (CSS) и поведения (JavaScript);
- использование JavaScript для повышения удобства использования уже рабочего приложения;
- применение техники Graceful degradation — если браузер не поддерживает те или иные функции, которые мы добавляем в приложение с помощью JavaScript — приложение всё равно остается рабочим.
В этой же статье я хотел бы рассказать об алгоритме реализации принципов «ненавязчивости» на JavaScript.
Сверстать страницу и реализовать какой-то серверный функционал — относительно просто. Сложно — построить фундамент для работы 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, связанный с DOM-элементом. Для простоты можно сказать, что Компонента и DOM-элемент — это одно и тоже.
Компоненты могут:
В HTML-коде, приведённом выше можно было бы выделить 7 компонент:
Команда «The Exceptional Performance» из Yahoo разработала набор правил для создания быстрых Web-страниц. Список включает в себя 34 пункта, объединённых в 7 категорий. Принципы Unobtrusive JavaScript не конфликтуют с этими правилами, наоборот — их цели похожи. Поэтому некоторые пункты Алгоритма будут подкреплены ссылками на эти правила:
Говоря «класс» я имею ввиду Class в понимании классического ООП. Использование такого подхода было продиктовано необходимостью повторного использования кода: вызовом конструкторов и деструкторов родительских классов после создания объекта и перед его уничтожением. Описание того, как происходит наследование в моих «классах» я выложил в моей статье-приложении «Классы в JavaScript: вызов методов родительского класса».
Возвращаясь к Компонентам, я бы хотел показать иерархию классов, имеющуюся в данный момент:
JooS.Class | +- JooS.TextElement | +- JooS.Element | +- Beatle.Class | +- Любой конечный Класс Компонента
JooS.Class — Абстрактный класс, общий предок.
JooS.Element — Класс «DOM-элемент», содержит свойство htmlElement и методы работы с ним
Beatle.Class — Абстрактный класс «Компонент», содержит методы совместной работы компонент
Также, специально для этой статьи, я подготовил страничку с готовыми компонентами: http://beatle.joos.nnov.ru/. Страница состоит из трёх частей:
О принципах совместной работы нескольких компонент будет рассказано ниже на примере «Формы комментария».
Совместная работа Компонент подразумевает под собой «обмен информацией». Способ, в котором компоненты зная друг о друге, напрямую вызывают функции соседней компоненты не подходит, т. к. это противоречит третьему принипу Unobtrusive JavaScript — соседней компоненты просто может не быть на странице и вызов вида this.neibourComponent.method1() вызовет ошибку JS. Проверки существования соседей для такого случая так же не подходят — таких проверок может быть очень много + заранее неизвестно сколько компонент захотят знать эту информацию.
С другой стороны необходимость в обмене информацией возникает внутри компоненты только при изменении состояния компоненты, например, после клика на её DOM-элемент. Назовём такое изменение состояния «Событием компонент» (далее — просто «Событие»). Такие События характеризуются идентификатором (именем), входными параметрами и функцией обработки входных параметров (например, для преобразования строки «10px» в число 10).
Назовём компоненту, генерирующую событие, «Бросающим», а компоненту, подписанную на это событие, — «Слушателем». Назовём «Диспетчером» компонент, через который «Бросающий» передаёт событие «Слушателям». Для того, чтобы передать событие, «Бросающий» и «Слушатель» должны сначала зарегистрироваться у «родительского-компонента-диспетчера» и только после этого начать передачу. Такая регистрация происходит в конструкторе компонента.
У меня получилось так, что любой компонент может выступать в роли «Бросающего», «Слушателя» и «Диспетчера» одновременно. Это вызвало долгие споры в нашей команде, но в конце концов все согласились, что структура кода HTML не только позволяет, но и диктует такой вариант решения.
Практически всегда «Слушатель» ожидает событие от своего соседнего компонента, и никак не от другого такого-же, но находящегося в другом конце страницы: например, компонента-валидаторФормы может ждать событие «форма заполнена» только от своей дочерней компоненты-FORM, а компонента-FORM ожидает события изменения значения только от своих дочерних компонент-INPUT-ов. Таким образом, возникает необходимость в нескольких диспетчерах событий + для работы системы без сбоев должен существовать хотябы один диспетчер «по умолчанию».
Побочным эффектом такой системы стала возможность «бросать событие в себя» или «слушать событие у себя» и необходимость регистрироваться у «родительской-компоненты-чёткоОпределённогоКласса» или у «строго родительской-компоненты».
Кроме случая, когда инициатором события является «источник информации», существует вариант, когда в роли инициатора может выступать «потребитель информации». Это решается так же, через механизм событий:
Пример: компонент-FORM бросает валидатору событие «форма заполнена»; валидатор в случае ошибки возвращает объект-описание-ошибки.
Вот такой вот алгоритм. Быть может с виду он покажется вам сложным, но на самом деле имея подобные инструменты у себя под рукой можно быстро создавать довольно сложные вещи. Это один из редких случаев, когда, казалось бы, искусственные ограничения и сложности («семантическая вёрстка» и «функциональное оформление») дают большой прирост в производительности команды и результата её работы.
Говорят, что JavaScript замедляет Web. Я же этой и предыдущей статьёй хотел показать, что всё в наших руках: мы можем сделать Web быстрее! (ну, или хотябы, создать видимость =)
Спасибо за внимание.