Сложные интерфейсы на javascript вместе Yahoo UI. Часть 5

October 10, 2008Comments Off on Сложные интерфейсы на javascript вместе Yahoo UI. Часть 5

Назначение (YUI) Yahoo User Interface – создание “серьезных и больших” веб-приложений. Этого не возможно достичь, только используя визуальные элементы управления (тема предыдущих статей). Кнопки, закладки, таблички и менюшки – все это хорошо и красиво, но главный вопрос остался открытым. Где взять данные для отображения в той таблице, как обработать и сохранить информацию, введенную пользователем в форму? Сегодня мы начинаем знакомиться с тем как YUI умеет “разговаривать” с серверными скриптами (на примере php).

В прошлый раз я закончил рассказ на том, как создать с помощью YUI форму, наполнить ее элементами управления и создать функции обрабатывающие событие “форму нужно отправить на сервер”. Форма сама умеет отправлять данные на сервер, как если мы указали режим для ее работы “postmethod=form” (т.е. синхронно), так и умеет отправлять данные асинхронно “postmethod=async”. При отправке данных асинхронно внутри объекта Dialog-а используется специальный класс, отвечающий за “отправку данных” – ConnectionManager (этот класс можно использовать и напрямую, без формы). Для того чтобы лучше понять то, как устроен Dialog и как можно влиять на его поведение (как обрабатывать различные ситуации, например, “начата отправка” или “сбой отправки”), нам нужно познакомиться сначала с ConnectionManager, затем я вернусь к Dialog-у. Итак, предположим, что на html-странице расположены два текстовых поля, для ввода имени пользователя и его возраста (просто поля - никакой формы). Заполнив сведения и нажав кнопку отправки, пользователь увидит перед собой список рекомендуемых, например, фильмов. Этот список сформирует размещенный на сервере php-скрипт. Для того чтобы создать объект ConnectionManager сначала нужно загрузить в html-страницу его код. Используем для этого старый добрый YUI-Loader:
  1. loader = new YAHOO.util.YUILoader();
  2. loader.require(["container", "button", "animation", "connection", "selector"]);
  3. loader.loadOptional = true;
  4. loader.base = 'js/';
  5. loader.insert( { onSuccess: doNothing } );
Теперь в теле html-документа я размещаю следующие два текстовых поля для ввода имени и возраста, а также кнопку отправки данных на сервер:
  1. fio: <input type="text" id="txtfio" /> <br />
  2. age: <input type="text" id="txtage" /> <br />
  3. <input type="submit" onclick="doSubmit()" />
Следующим шагом внутри функции doSubmit я должен получить значения введенные пользователем в текстовые поля. Для тренировки я сделаю это двумя способами: первый из них предполагает вызов метода YAHOO.util.Dom.get, в качестве параметра которого передается идентификатор первого элемента (ttxfio). Второй прием использует возможности selector-api. Чтобы освежить свои воспоминания о классе YAHOO.util.Selector обратитесь к первой статье серии:
  1. function doSubmit(){
  2.  var fio = YAHOO.util.Dom.get('txtfio').value;
  3.  var age = YAHOO.util.Selector.query('#txtage')[0].value;
  4.  var url = "post.php?fio="+fio+"&age="+ age;
  5.  var callback = {
  6.      success: onLoadOk, 
  7.      failure: onLoadFail, 
  8.      argument: {task: 'sending user info'}
  9.  };
  10.  r = YAHOO.util.Connect.asyncRequest('GET', url, callback); 
  11. }
Получив значения полей fio и age, я конструирую строку url, которая содержит как адрес вызываемого php-скрипта “post.php”, так и значения переменных для его работы. Способ отправки данных на сервер (метод GET или POST) задан как первый параметр при создании объекта запроса (r). Второй параметр метода asyncRequest - адрес загружаемой страницы, а вот третий параметр гораздо сложнее предыдущих. Объект callback служит для того, чтобы назначить callback-функции, вызываемые после того, как ответ от сервера был успешно получен (success), и в том случае, если отправка данных завершилась неудачно (failure). Т.к. в один момент времени мы можем отправлять несколько запросов к одному и тому же скрипту и, соответственно, результаты обработки будут поступать в одни и те же функции (success и failure), то нам нужен способ их отличать между собой. Для этого как раз и служит такой элемент объекта callback как “argument”. Это произвольный объект, который будет передан в функцию-обработчик среди таких параметров “код http-ответа”, “сформированный php-скриптом текст” (внимание на o.argument.task):
  1. function onLoadOk (o){
  2.  alert ('status='+o.status);
  3.  alert ('statusText='+o.statusText);
  4.  alert ('responseText='+o.responseText);
  5.  alert ('argument='+o.argument.task);
  6. }
Кроме показанных в примере переменных status (код ответа, например, 200), responseText – собственно результат выполнения php¬-скрипта, можно обратиться еще и к переменной responseXML. Это пригодится вам в случае, если серверный скрипт отдает результаты обработки данных в формате xml, например, если ваш сайт читает rss-ленту новостей с сайта. Не забывайте, что общие правила безопасности запрещают javascript загружать данные с сайта отличного от “домашнего”. А, следовательно, нужно создать php-скрипт играющий роль proxy (посредника), загружающего rss-поток с сайта и возвращающего их обратно в javascript код. Для того чтобы результат загрузки ленты (с помощью функции file_get_contents) был воспринят как xml во всех браузерах необходимо правильно указать http-заголовки для типа документа (за это отвечает функция header):
  1. <?php
  2.   header ('Content-type: text/xml');
  3.   print file_get_contents ('http://rss-here.ru);
  4. ?>
После того как xml-данные были загружены ConnectionManager-ом, вы должны выполнить их анализ с помощью … с помощью чего? Не секрет, что наиболее популярной и известной технологией для поиска информации, выбора узлов xml-документа на основании некоторых условий является xpath. К сожалению, поддержки xpath всеми браузерами нет. Может ли нам предложить какие-то средства поиска в xml-е сама библиотека YUI? Еще раз нет: так знакомый нам по первой статье серии класс YAHOO.util.Selector не умеет выполнять поиск внутри xml-документа, а только html (как ни странно). Для тех, кто знаком с jquery (об этой библиотеке я писал серию статей еще прошлым летом) это довольно обескураживающее т.к. большой разницы между xml и html нет, и, например, jquery умеет искать информацию и в xml и в html. К счастью, написать собственную простенькую функцию анализатора xpath не сложно, или попробовать найти такую в internet – задача работы с xpath в javascript совсем не нова. В крайнем случае, для работы с xml можно использовать старые добрые, пусть и не удобные, функции getElementsByTagName и getElementById.

В списке наиболее популярных форматов данных, которыми обмениваются при разработке ajax-основанных сайтов серверный скрипт на php и javascript-код входят “просто текст” (text/plain), xml (text/xml) и json. Обладая удобочитаемостью, компактностью записи и способностью к кодированию самых сложных структур данных, JSON бьет в этом плане и xml и plain text. К сожалению специального свойства, вроде responseJSON, нет. Так, что если данные пришли в формате json, то вам нужно выполнить его “раскодирование” с помощью встроенной javascript-функции eval. Предположим, что есть следующий php-скрипт, выводящий данные в формате json (для преобразования структур данных в стиле php в формат json я использовал php-функцию json_encode):
  1. // правильно укажем тип документа
  2. header ('text/javascript');
  3. // входные данные
  4. $data = array ('fio'=>'jim', 'age' => 12);
  5. // теперь кодируем их
  6. print json_encode ($data);
В результате отправленные сервером данные будут выглядеть так:
  1. {"fio":"jim","age":12}
Теперь нужно внутри функции обработчика события “success” строку responseText вычислить, предварительно окружив круглыми скобками (иначе JSON выражение будет некорректно):
  1. function onLoadOk (o){
  2.  var code = "("+o.responseText+")";
  3.  code = eval (code);
  4.  alert (code.fio);
  5. }
В результате переменная code будет хранить ассоциативный объект в точности соответствующий тому, что был сформирован в php-скрипте. Подведем итог: мы научились использовать yui объект ConnectionManager для работы с тремя основными форматами данных: text, xml, json. Вот только, реальное приложение не только должно “принимать и обрабатывать” информацию с сервера, но и “подготавливать и отсылать”. Вспомните то, как в самом начале статьи я послал на сервер запрос к файлу post.php, передав при этом ему две переменные:
  1. var url = "post.php?fio="+fio+"&age="+ age;
Такой способ отправки данных (метод GET, когда данные для скрипта передаются в адресной строке) неудобен, приводит к появлению потенциальных ошибок и совершенно не применим в случае, если мы хотим отправить на сервер большой пакет информации: из-за ограничений на длину адресной строки. Решением является использование метода POST (когда переменные будут переданы на сервер не в адресной строке, а в теле http-запроса):
  1. var postData = "fio="+fio+"&age="+ age;
  2. r = YAHOO.util.Connect.asyncRequest('POST', url, callback, postData);
Даже поверхностного взгляда на приведенный код хватает чтобы сказать: он ужасен. Основная проблема в том, что мы вынуждены, как и в случае с “GET”-запросом формировать длинную-предлинную строку, содержащую значения всех отправляемых на сервер переменных. В случае если нужно передать что-то сложное, например, массив объектов, то алгоритм формирования строки запроса ой как не тривиален. Нам поможет полный отказ от идеи с кодированием данных в виде показанной выше строки и переход к использованию формата “от рождения” предназначенного хранить сложные структуры данных. И это JSON, не зря же буковка j в этой аббревиатуре означает javascript. Остался только открытым вопрос о том, как JSON структуру данных перевести в форму строки. К счастью разработчики yahoo ввели в состав yui модуль json. В его составе есть только две функции: parse и stringify. Первая из них предназначена для того, чтобы строку перекодировать в json-структуру данных. На самом деле внутри этой функции просто перевызывается стандартная для javascript функция eval, с которой я вас познакомил чуть ранее. А вот функция stringify нам пригодится: ее назначение – это как раз преобразование json в строку. Таким образом, код отправки данных на сервер будет выглядеть следующим образом (не забудьте только добавить модуль json в список загружаемых в начале программы):
  1. // отправляем на сервер массив объектов, каждый из которых описывает сведения об некотором человеке
  2. var people = [
  3.   {fio: 'Jim', age: 12}, 
  4.   {fio: 'Tom', age: 13}
  5. ];
  6. var postData = "json="+ YAHOO.lang.JSON.stringify (people);
  7. r = YAHOO.util.Connect.asyncRequest('POST', url, callback, postData);
Кроме первого параметра (json объекта подлежащего кодированию) функция stringify может принять еще два необязательных параметра. Наиболее полезен из них второй – это массив, элементы которого играют роль “белого списка”. Т.е. если в предыдущем примере я хочу отправить на сервер не все сведения о персонах, а только значение поля “fio”, то можно сделать так:
  1. var postData = "json="+ YAHOO.lang.JSON.stringify (data, ['fio']);
После отправки данных на сервер, самое время задуматься над тем, как php сумеет правильно декодировать пришедшие json данные. Используем для этого функцию json_decode:
  1. $json = json_decode($_REQUEST['json']);
В последнее время в веб-программировании все отчетливее слышатся голоса тех, кто попробовал REST стиль разработки и кому понравилось. Разговор об идеях лежащих в основе REST и их конкретной реализации тянет на отдельную и довольно большую серию статей, так что я не буду углубляться в детали, а нарисую небольшой сценарий. Предположим, что вы решили создать в форме веб-приложения, да хоть, кухонный справочник. Сначала вы сделали это в стиле “никакого ajax”. Т.е. написанные вами серверные скрипты на php формируют цельнолитые html-страницы. Затем вы решили перевести приложение на “ajax” и скрипты php нужно переписать с учетом новых требований так, чтобы результатом их работы была не целая html-страница, а только данные, например, в json-формате. Затем вы решили создать flash-сайт для того же кухонного справочника, и теперь данные передаются в форме xml или amf. В каждом из трех случаев серверная часть приложения имеет общие стороны. Фактически до момента “вот мы отобрали из базы данных информацию о рецепте и теперь ее надо отправить клиенту” все абсолютно идентично. Отличия только в визуализации информации. Для тех, кто знаком с паттерном MVC может показаться очевидным, что “C” контроллер (то, что ищет сведения об кухонных рецептах в БД) будет общим для трех приложений, а отличия будут в наличии трех разных реализаций “V” (view/представления). Одно представление “обертывает” информацию в html-шаблон, например, с помощью smarty, второе представление преобразует данные в json-форму, да хоть с помощью описанной выше функции json_encode, и третий объект “представления” кодирует информацию в xml (для чего в php также найдется парочка стандартных функций). Остается только придумать и передать на сервер специальный “флажок”, указывающий контроллеру то, какой view нужно использовать в данном конкретном случае. Самым простым и одновременно плохим способом будет передача в php-скрипт специальной переменной, значением которой является ожидаемый формат данных, например:
showRecept.php?recept_id=macaroni&format=html
Или так:
json/recept/macaroni/
Во-первых, плохо наличие в адресной строке обязательной и совершенно бесполезной для клиента добавки с указанием формата документа “html”. Во-вторых, это плохо тем, что плодит множество точек входа в приложение, некоторые из которых совсем не предназначены для прямого просмотра. Например, для ajax приложения вызов серверного скрипта по адресу “showRecept.php?recept_id=macaroni&format=json” имеет смысл только при работе из javascript-кода, который знает, что с этими данными делать и как их интерпретировать. Пользователь же, набрав подобный адрес, увидит “абракадабру”. Особо болезнен вопрос с поисковыми машинами, которые в поисках информации могут проиндексировать “темную сторону вашего сайта”. И не говорите мне, что поисковики не сканируют javascript-код – еще как сканируют, и формы уже научились отправлять – то ли еще будет. Как вывод: поисковику и посетителю без установленного flash плеера или с отключенным jaavscript нужно отдать одно содержимое – классическую html-страницу без ajax-плюшек. Для более “продвинутых” посетителей – данные должны передаваться с сервера в flash-ролик или jaavascript-приложение в формате json или xml. И все это должно работать под единым адресом: /recept/macaroni/. Так как же передать на сервер “маркер” того какой формат для нас нужен сейчас? Причем кодировать это в адресной строке запрещено. Решение основано на использовании http-заголовков. Не секрет, что когда вы набираете в адресной строке браузера http://my.site, то браузер посылает серверу массу дополнительных сведения, например, какая версия операционной системы у клиента, какой браузер используется, предпочитаемый язык, cookie и еще многое другое. Так почему бы не отправить среди эти стандартных заголовков свой пользовательский, например, такой:
cook-book-format: json
PHP-скрипт на сервер проверит значение заголовка и вернет информацию в нужном формате (создаст правильное view). Если же заголовок не указан, то мы предполагаем обращение к сайту робота поисковой машины и формируем обычную html-страницу без “плюшек”. Теперь рассмотрим как в yui можно это реализовать:
  1. YAHOO.util.Connect.resetDefaultHeaders ();
  2. YAHOO.util.Connect.initHeader ('MySpecialHeader1', 'HeaderValue1');	  
  3. YAHOO.util.Connect.initHeader ('MySpecialHeader2', 'HeaderValue2');	  
  4. r = YAHOO.util.Connect.asyncRequest('POST', url, callback, postData);
Первым шагом я выполнил очистку заголовков запроса от устаревших артефактов (функция resetDefaultHeaders). Затем с помощью вызова initHeader я добавляю нестандартные заголовки к запросу. Теперь на стороне сервера я могу узнать какие данные были отправлены мне:
  1. // массив полученных заголовков
  2. $headers = getallheaders();
Еще остался открытым вопрос об отправке на сервер с помощью асинхронных запросов файлов. Давно уже прошло то время, когда данная операция считалась не возможной. Есть две методики решения задачи асинхронной отправки файлов на сервер. Первая из них предполагает использование скрытого фрейма (iframe) содержащего внутри себя форму с полем типа “file”. Вторая стратегия отправки предполагает использование flash-“прослойки”. Среди минусов этого решения необходимость поддержки клиентом flash (совсем не проблема) а в качестве бесплатных бонусов вы получаете средства мониторинга статуса загрузки файлов (так можно вывести полоску прогресса, показывающую сколько времени прошло и сколько осталось). YUI поддерживает два этих способа и следующий пример как раз показывает прием с iframe. Для начала я создаю html-форму с полем выбора файла (значение атрибута action большого значения не имеет):
  1. <form action="upload.php" enctype="multipart/form-data" method="post" id="uForm">
  2.   <input type="file" name="fileField"/>
  3.   <input type="button" onclick="doSubmitFile ()"/>
  4. </form>
Теперь нужно внутри обработчика события “отправка формы” присоединить объект формы к ConnectionManager-у. дальнейшие действия по отправке идентичны показанным ранее за исключением того, что для отслеживания ситуации “запрос к серверу был завершен” нужно использовать не показанную ранее success, а функцию upload:
  1. function doSubmitFile (){
  2.  YAHOO.util.Connect.setForm('uForm', true);
  3.  var callback = {upload: onUploadComplete};	
  4.  r = YAHOO.util.Connect.asyncRequest('POST', 'form.php', callback);  
  5. }
  6.  
  7. // и теперь обработка события “запрос завершен”
  8.  
  9. function onUploadComplete (o){
  10.  alert (o.responseText);
  11. }
При отправке больших объемов данных или работе с высоконагруженными сервисами следует уделить внимание параметру timeout, т.е. предельному времени в течении которого запрос должен быть обработан. Для этого я создавая callback-объект должен указать параметр timeout (количество миллисекунд):
  1. var callback =  {success:onSuccess, failure: onFailure, timeout: 2000};
Если выполнение запроса заняло более двух секунд, то управление будет передано функции onFailure. А для того, чтобы отличить ситуацию “timeout” от любых других ошибок, YUI установит значения полей объекта response.status равным -1, а поля response.statusText равным фразе “transaction aborted”.