PHP программисты очень не любят доверять логику приложения javascript. Они приходят в ужас от одной мысли об изменении веб-формы на стороне клиента. В крайнем случае, они, с крайней опаской, готовы "в перчатках jQuery" прикрутить к сайту какой-нибудь лайтбокс. Огромные резервы клиентской части приложения остаются неиспользованными...

javascript внутри веб-приложения

Проблема работы с javascript состоит в том, что сервер ничего не знает о клиенте. Сервер отдал по запросу HTML-страницу, сопутствующие файлы css и js, а уж как оно там дальше будет работать, наверняка никогда не знаешь. Основной вопрос философии для javascript – как сообщить браузеру, что при обращении пользователя к DOM-элементу (например, при щелчке мышью по картинке) надо вообще запускать javascript, и какую именно функцию?

1. Самый очевидный вариант здесь – добавить при генерации HTML атрибут: <img onclick="showBig()" src="tumb.png">. Такой вариант не обеспечивает наличия самой функции, которая должна сработать при щелчке мышью. И если вдруг мы забудем присоединить к странице соответствующий файл js, браузер начнёт сообщать об ошибках.

2. Приятнее выглядит вариант, когда сам javascript, уже на стороне клиента, навешивает события на некоторые элементы (javascript с большей вероятностью знает, есть ли у него подходящая функция). Остаётся вопрос: почему бы это javascript вдруг начал связывать элементы с событиями? Можно ответить на него, например, так:

2.1. Когда нам нужно ко всем картинкам страницы привязать функцию обработки на клиенте, мы просто добавляем в шаблон страницы метку: <script type="text/javascript" src="kartinki.js"></script>, а в файле kartinki.js есть функция, которая при загрузке страницы проходит в цикле все изображения и что-то с ними делает.

2.2. Решение по типу 2.1. плодит множество мелких файлов js, что плохо для производительности приложения. Поэтому стратегически правильнее всегда загружать с каждой страницей сайта все js-функции, которые когда-либо могут на сайте понадобиться, – и желательно всё это в одном-двух файлах. Казалось бы, такая глобальная загрузка вроде бы должна решить проблему "недостаточности" варианта 1 и сгладить разницу с вариантом 2. Но это не совсем так. В варианте 1 в случае ошибки программиста пользователь будет видеть ошибки браузера, а в варианте 2 просто не будет ничего происходить (как будто и не было запланировано).

В варианте 2 остаётся ещё вопрос локализации: не все картинки сайта выводятся на страницы в уменьшенном виде и имеют большую копию, загружаемую при щелчке. Если функция prepareBigImg() будет вызываться всегда при загрузке страницы и обрабатывать все картинки, мы опять же будем видеть ошибки в браузере. Поэтому для картинок-миниатюр должен быть какой-то маркер – например, className. То есть всё равно при генерации HTML-кода элементы надо как-то помечать (даже если не навешивать им сразу события), и вместо onclick="showBig()" нужно будет писать что-то вроде class="thumbnail", а в функцию должен быть зашит отбор элементов по признаку (className == "thumbnail").

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

Так-то вроде пофиг, но где-то всё равно должна быть лажа зарыта, и где-нибудь это «ковровое бомбометание» когда-то аукнется. Лучше бы знать точно, что на странице есть нужные объекты. Достаточно очевидным кажется передавать для этого в DOM какой-то маркер – например, id элемента-контейнера. Тогда функция инициализации изменилась бы примерно так:

...и становится непонятно, что делать, если на странице две таблицы или две полоски миниатюр (вверху и внизу) – по атрибуту id ведь можно найти только один элемент. Использовать className для контейнеров нет смысла – проще уж тогда искать по className напрямую сами элементы. Можно, например, делать все контейнеры именованными и проверять их все:

Или передавать какую-то глобальную метку, сообщающую, что на странице "есть какие-то контейнеры для элементов каталога" и надо прошерстить все элементы страницы. Как бы ни хотелось этого избежать, но глобальные метки уместнее всего передавать в HTML-коде страницы скриптом:

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

Такая связка кажется надёжной и удобной для генерации: функция генерации каталога (или меню) на сервере ведь знает, что именно она генерирует (catalog или menuTop), и она может проставить случайные id контейнерам и записать эти id в набор маркеров (в момент генерации – в виде массива, а при выводе страницы – уже построить из массива js).

Если уж так жалко девственную чистоту HTML, можно, в принципе, вынести набор маркеров в отдельный файл. На сайте используется конечное количество шаблонов, и каждому шаблону может быть сопоставлен свой файл js с маркерами. Но это уже будет менее надёжно, чем инлайн скрипт (так как обработчик может быть привязан не только к шаблону).

Что такое нормализация?

В общепринятом понимании термин «нормализация» обычно используется для приведения к нормальной форме реляционной базы данных. Нормальная форма обеспечивает более надёжное управление данными. Процесс нормализации напоминает вынесение повторяющихся частей «за скобки». Например, есть таблица Заказы:

При занесении в эту таблицу клиентов возможны ошибки (опечатки), и один и тот же клиент будет записан по-разному, что приведёт к ошибкам в дальнейшей обработке. Поэтому лучше вынести клиентов в отдельную таблицу Клиенты:

А для поля klient в таблице Заказы создать внешний ключ в таблице Клиенты и таким образом контролировать целостность данных (единообразие в написании одного и того же клиента).

***

В широком смысле, слово «нормализация» может быть применено также и к нашим действиям по управлению javascript со стороны веб-сервера. Ведь мы пытаемся обеспечить более надёжное управление клиентскими скриптами со стороны сервера. Нормализация всегда связана с увеличением количества сущностей. У нас эти сущности разрастаются прямо-таки катастрофически: вместо одного onclick получилось:

1) className для элемента (например, для миниатюры в Каталоге);

2) id для контейнера (например, для фрагмента Каталога или для Меню);

3) dict.markers – контейнер в Словаре, в котором Сервер передаёт для обработчиков javascript информацию о контейнерах на странице;

4) наименования самих обработчиков javascript (совпадающих, по идее, с Сущностями, определяемыми в Модели на Сервере – "catalog", "menu"...).

Нормализация варианта 1

А нельзя ли обеспечить надёжное управление с помощью варианта 1 (<img onclick="showBig()" src="tumb.png">)? Ведь мы ещё не пробовали «нормализовать» его. В варианте 2 мы, по сути, пытаемся передать от сервера клиенту следующую информацию: «На странице есть некоторые объекты (список прилагается), которые должны реагировать на поведение пользователя. Если у вас есть подходящие обработчики (см. список), примените их». Клиент может проверить наличие обработчиков и, если обработчика нет, спокойно ничего не делать.

Вариант 1 (без нормализации) говорит следующее: «При щелчке пользователя мышкой обработайте элемент функцией javascript showBig». У клиента нет выбора, нет возможности проверки, существует ли функция. Клиент тупо пытается выполнить функцию (которая, возможно, не существует). Здесь возможно подготовить некоторое «сглаживание»: передать в Словарь список функций, существование которых следует проверить, и если функции нет, создать пустую (заглушку для ошибок). Что-то вроде этого:

При «нормализации» варианта 1, как видим, добавляется всего одна лишняя сущность (контейнер проверяемых функций в Словаре).

Почем же мы выбираем вариант 2?

Вариант 1 слишком жёстко связывает поведение javascript, Сервер вынужден очень глубоко вникать во внутреннюю логику клиентских скриптов. В простых случаях (как клик по картинке) это некритично. Но, например, при обработке полей формы легко увязнуть в неразрешимых противоречиях. Противоречие, строго говоря, только одно: для Сервера, по сути, создаётся какое-то подобие API по работе с javascript, но применить его невозможно из-за рассогласования во времени, Сервер не может своевременно получать ответы команд, которые он даёт Клиенту.

Вариант 2 открывает очень широкие возможности. Его идея – передавать Клиенту часть логики (и логики Приложения, и Предметной или бизнес-логики) в виде словарей (массивов) и отдельно передавать маркеры объектов (к которым логика должны применяться), а javascript уже своими средствами (о которых Сервер знать не должен) реализует эту логику. Принцип отделения логики от вычислений для нас намного важнее, например, отделения php от HTML. Мы всегда сохраняем значительную часть логики приложения в виде данных и необходимое количество этих данных передаём клиенту в формате js-объектов.

D.M., admin

Комментарии

лесник 29.08.2012 20:06:17

Алексей, а что говорит о таких приёмах валидатор? Это уже не HTML, а какой-то XML получится. Ну, валидатор, в общем-то, конечно, пофиг. Но идя в этом направлении, мы будем забивать данные метками "действий", – фактически, внедрять в данные код (чего я очень стараюсь избегать).

Алексей 22.08.2012 21:35:08

Про передачу глобальных меток: опытным путём определил (для себя) что атрибуты можно добавлять любому объекту не только программно, а даже в тексте HTML. Тоесть коли сервер пишет строку типа <input ... id="cat1" ..>, можно сразу накатать <input ... id="cat1" marker="catalog" ..> и потом при обращении к этому элементу по id, в зависимости от его атрибута marker, выполнять с ним действия (вешать обработчик событий и т.д.)