Введение в технологию ajax. Часть 2

October 1, 2007

Я продолжаю серию статей посвященных технологии ajax. В прошлый раз я рассказал об истории развития ajax, о том, как поддержка методов “подзагрузки” информации в страницу проделала путь от специфической технологии, поддерживаемой только internet explorer, до общепризнанного стандарта в web. Я рассказал об “старых” методиках – применявшихся до того как поддержка ajax добавилась в opera9 и firefox. Я упомянул о существовании библиотеки Д. Котерова и ее подходе к реализации ajax. Сегодня я расскажу вам о библиотеках иного плана. Их цель не обеспечить “просто отправку и загрузку данных” а, именно, обеспечить высокоуровневый слой-посредник между javascript-кодом, который исполняется в вашем браузере, и php-кодом работающем на веб-сервере. Причем, и это важно, такая коммуникация должна быть “прозрачной” или незаметной – т.е. обоим сторонам будет казаться что вызываемые ими функции находятся “вот здесь рядом”, а не за тысячи километров.

В прошлый раз я закончил на том, что показал, как именно можно выполнить ajax вызов и загрузить в страницу некоторый код xml. Затем данный код анализировался, и его содержимое применялось для изменения внешнего вида страницы. Я опустил вопрос о том, откуда взялся этот самый xml, кто его сформировал. Интуитивно нам все ясно, что код должен был быть сгенерирован некоторым php-скриптом посредством множества вызовов функций print ‘еще кусочек xml’. Например, так:
  1. <?php
  2.   header ('Content-Type: text/xml');
  3.   // важно не забыть указать тип возвращаемой информации
  4.   print ('<?xml version="1.0" encoding="windows-1251"?>' . "\n");
  5.   print ('<result><color>red</color><message>Запрос выполнен успешно</message></result>' . "\n");
  6. ?>
Наверное, здесь вообще не стоило останавливаться. Благо, единственная сложность, которая вас ждет - при выводе xml не ошибиться в записи собственно тегов – таких вольностей (перепутанные и незакрытые теги, беспорядок с регистром символов) как html xml не позволяет. Можно построить дерево xml с помощью специальных функций, вроде: “createElement, appendChild” и т.д., затем данное дерево с помощью опять-таки служебной функции php будет преобразовано в текстовый вид. На этом пути вас ждут проблемы с тем, какую библиотеку для работы с xml использовать (в мире php в настоящее время можно насчитать добрый пяток разномастных библиотек, зачастую не совместимых между собой). Можно использовать не встроенные в php функции для работы с xml, а пользовательские библиотеки, например, domit. Их особенность в том, что они реализованы не на языке “c/c++”, в виде библиотек dll, а написаны на самом php – это дает огромные потери по скорости работы, но дает независимость от особенностей используемого хостинга. Но все же, главная проблема – это необходимость преобразования данных из формата, которым оперирует php, – массивы, объекты, простые скалярные переменные – в тот формат, который был бы способен понять браузер и javascript. На стадии приема данных из скрипта javascript для последующее обработки внутри php-кода стоит та же проблема – необходимость уже двухшаговой конвертации данных. Смотрите, данные, которые мы хотим послать из браузера, хранятся в некоторой структуре понятной javascript – массивы, объекты, скаляры. Мы их кодируем в формат xml, и пересылаем на сервер. Там выполняется преобразование из формата xml во множество переменных php, тех же массивов, объектов и скаляров. Возникает вопрос: а можно ли исключить из этой цепочки xml и обеспечить прямую передачу данных из javascript в php? Естественно, что этим вопросом озаботились достаточно давно и среди множества библиотек поддержки ajax можно найти те, которые выполняют такое преобразование не заметно для нас. В основе своей все равно идет передача xml по сети, но мы об этом просто не знаем. Перед тем как я покажу одну из подобных библиотек, стоит упомянуть об ajaj. Термины ajax и ajaj отличаются только одной последней буквой – собственно она кодирует то, какой формат будет использован для передачи данных. На самом деле, когда вы выполняете запрос из страницы, то формат может быть любым – абсолютно – стандарт http почти никак это не ограничивает. Главное, чтобы этот формат могли бы понять на той стороне – сервере. Просто так исторически сложилось, что первым на эту роль пришел xml. Xml давно и заслуженно (зачастую переигрывая) критикуют за сложность. Все эти скобки, вложенные-переложенные теги пугают молодых неофитов. Да и действительно, xml слишком универсален, и не удобочитаем. В ситуациях, когда передаваемые данные не велики и представляют собой простое перечисление элементов или набор свойств некоторого объекта, то xml выглядит слишком громоздко. Поэтому некоторое время назад появился язык yaml, который пытаются позиционировать как замену xml. Уверен, что очередной революции не случится. Yaml занял свою скромную нишу как язык написания несложных конфигурационных файлов, как формат передачи небольших и не очень сложно-структурированных документов, но на большее он не способен. На этом отступление от темы я закрою, а теперь собственно вопрос. А что нет такого формата, в который можно было бы легко перекодировать данные из javascript-кода. А затем, хотя это и не столь обязательно, можно было бы быстро прочитать и разобрать в php-коде? Конечно, такой формат есть! Он называется json. Вот, та последняя буква в аббревиатуре ajaj – asynchronous javascript and json. JSON – это еще одна аббревиатура, раскрывающаяся как javascript object notation. Эта форма нотации позволяет в достаточно компактной и удобочитаемой форме записать сложную структуру данных в javascript или flash actionscript . Родной поддержки json в php нет, но к счастью в php5 появились функции для быстрого (это важно) разбора json переменных и преобразования их в переменные в стиле php и наоборот. На всякий случай предварительно проверьте включаена ли поддержка json у вас на хостинге. Так создав файл php, который содержит вызов только одной функции “phpinfo ();” можно получить подробное перечисление поддерживаемых расширений см. рис. 1.



А теперь пример использования json-функций:
  1. <?php  
  2. // исходная информация для последующего кодирования - 
  3. // обратите внимание,что мы используем сложные типы данных:
  4.  // ассоциативный массив, содержащий вложенный массив объетов (payments)
  5.  $x = array ( 
  6.    'fio' => 'bill makkenzi',
  7.    'birthdate' => '2000.1.1', 
  8.    'payments' => array (
  9.       array ('dateof' => '2001.1.1', 'balance' => 3000 , 'currency' => 'EUR'),
  10.       array ('dateof' => '2002.1.1', 'balance' => 5700 , 'currency' => 'USD'),
  11.       array ('dateof' => '2003.7.1', 'balance' => -600 , 'currency' => 'EUR')
  12.     ) 
  13.  );
  14.  print '<h1>Сначала JSON из PHP-объекта</h1>';
  15.  print json_encode ($x);// создаем строку с json-нотацией
  16.  print '<h1>А теперь PHP из JSON-строки</h1>';
  17.  print '<pre>';// выполняем раскодирование json-строки в переменную php
  18.  print_r (json_decode (json_encode ($x)));
  19.  print '</pre>'; ?>
Результат работы скрипта показан на рис. 2.



А для разбора json-строки в коде javascript достаточно сделать вызов функции eval (функция вычисляет переданный ей аргумент – строку с json нотацией). Последнее - закодировать данные из javascript в json-строку текста для последующей отправки серверу - и цикл замкнется. Давайте разберем это на примере библиотеки jquery, я писал о ней в прошлых сериях, хотя большей частью сосредоточился на том, какие плюсы jquery дает для работы с деревьями DOM, с событиями элементов. Теперь осталось рассмотреть предпоследнюю главную возможность jquery – поддержку ajax. Возможно, я сделаю в последующем еще одну статью, которая будет посвящена анимации и спецэффектам в javascript с помощью jquery и других библиотечек, но об этом после. Задача будет проста – javascript-код делает вызов некоторого файла php, который читает размещенный на сервере файл, каждая строка которого, например, - имя пользователя оставившего сообщение и собственно, текст сообщения в гостевой книге сайта, разделенные символом “:”. Данные кодируются в json-формат, затем отправляются в браузер, где строится html-таблица с перечнем этих сообщений. Сначала пример файла с данными (baza.txt):
Vasya Tapkin: It's Super Site.
George Tailor: It's Very Bad Site.
Mary Tompkins: I know nothing about this site.
Теперь код файла php, обратите внимание, что в качестве параметра этому файлу передана переменная page_size, управляющая тем, какое количество первых сообщений из файла следует вернуть:
  1. <?php
  2.  $plaindata = file ('baza.txt');
  3.  // прочитали файл с сообщениями
  4.  $result = array ();
  5.  // входная переменная управляющая работой скрипта - количество сообщений которые нужно вернуть
  6.  $page_size = $_REQUEST ['page_size'];
  7.  for ($i = 0; $i < min($page_size, count($plaindata)); $i++){
  8.   // в цикле накапливаем пары (кто, сообщение) в массиве
  9.   list ($user, $msg) = explode (':', $plaindata [$i]);
  10.   $result [] = array ('user'=>$user, 'msg' => $msg); 
  11.  }
  12.  // здесь формируем окончательный массив с данными, отправляем его в кодированной форме браузеру
  13.  print json_encode (array ('filesize' => filesize ('baza.txt'), 'messages' => $result));
  14. ?>
Перед примером кода javascript пара слов о поддержке ajax в jquery. Функция выполняющая удаленный вызов называется $.ajax(). Она получает в качестве параметра только одну переменную – ассоциативный массив, содержащий множество ключей управляющих тем как именно будет выполнен запрос, какие данные и в каком формате должны уйти и вернуться назад, а также функции которые будут вызваны чтобы известить о завершении процесса обмена информацией. В следующей таблице я перечисляю некоторые из этих ключей.
Ключ Примечание
async Булева переменная, кодирует признак того, будет ли сделан удаленный вызов как синхронный или асинхронный. Если вызов синхронный то на момент выполнения запроса браузер будет заблокирован.
beforeSend Функция, которая вызывается перед отправкой запроса, ее назначение выполнить окончательную до-настройку объекта XMLHttpRequest находящегося внутри jquery.
complete Функция вызывается после завершения вызова, когда данные были возвращены, и не зависимо от того успешно, или нет произошел ajax-вызов.
contentType Сообщение серверу, что текстовые данные кодируются следующим образом.
data Собственно данные, которые отправляются серверу, это может быть как строка текста, так и массив, объект или их комбинация.
dataType Очень важный параметр, управляет тем, в каком формате данные будут возвращены сервером. Возможны следующие значения: xml, html, script, json.
error Функция вызываемая в том случае если удаленный вызов был неуспешен.
processData Возможность отключить кодирование данных перед отправкой. Очень специфическая функция – лучше не трогайте.
success Функция вызываемая, когда запрос был успешно завершен.
timeout Переменная кодирующая интервал времени в течении которого мы ждем завершения удаленного вызова. Задается в миллисекундах.
type Тип запроса: GET или POST. Остальные методы: HEAD, PUT – не поддерживаются.
url Самое главное – адрес страницы php, которую следует запустить.
А теперь код примера, загружающего книгу обратных отзывов, в теле страницы присутствует блок div, куда будет помещена создаваемая таблица сообщений, также текстовое поле, где будет указываться число – максимальный размер возвращенной таблицы сообщений:
  1. <html>
  2.  <head>
  3.   <script type="text/javascript" language="javascript" src="jquery.js"> </script>
  4.   <script>
  5.  
  6.    function sender (){
  7.     $.ajax({
  8.       type: "POST", url: "bazareader.php", dataType : 'json',
  9.       data: {
  10.         page_size: $('#page_size').attr('value') 
  11.       },
  12.  
  13.       success: function(msg){
  14.        var oRow = null;
  15.        var oCell = null;
  16.  
  17.        alert( "File Length: " + msg.filesize );
  18.        var t = document.createElement ('table');
  19.        // создаем первую строку таблицы с заголовками - названиями столбцов
  20.        oRow = t.insertRow(0);
  21.  
  22.        // создаем ячейку строки
  23.  
  24.        oCell = oRow.insertCell(0);
  25.        // и указываем содержимое ячейки
  26.        oCell.innerHTML = 'User';
  27.  
  28.        oCell = oRow.insertCell(1);
  29.        oCell.innerHTML = 'Message';
  30.        // организуем цикл по массиву сообщений
  31.        for (var i = 0; i < msg.messages.length; i++){
  32.          // создаем очередную строку
  33.          oRow = t.insertRow(i + 1);
  34.          // в нее помещаем две ячейки - для имени пользвователя 
  35.          oCell = oRow.insertCell(0);
  36.          oCell.innerHTML = msg.messages[i].user;
  37.          // и для текста сообщения
  38.          oCell = oRow.insertCell(1);
  39.          oCell.innerHTML = msg.messages[i].msg;
  40.        }//for
  41.        document.getElementById('dv_Result').appendChild (t); 
  42.      } 
  43.    }
  44.  );
  45. }// конец ajax вызова
  46.  
  47. </script> 
  48. </head>
  49.  
  50. <body>
  51.   <div id="dv_Result" style="">
  52.      Result ...
  53.   </div>
  54.    <br />
  55.    Page Size: <input type="text" id="page_size" value="10" />
  56.    <br />
  57.    <input type="button" onclick="sender ()" value="click me !"/>
  58. </body>
  59. </html>
Результат работы скрипта показан на рис. 3.



Естественно, что в качестве источника данных может выступать и произвольный скрипт, например, далее я выполняю загрузку с сайта tut.by новостей в формате rss. Данный формат основан на xml (следовательно, все, что мне нужно - это при вызове ajax указать значение свойства dataType как “xml”) и выглядит примерно так:
  1. <?xml version="1.0" encoding="windows-1251"?>
  2. <rss version="2.0">
  3.   <channel>
  4.    <title>Новости TUT.BY Главные новости</title>
  5.    <link>http://news.tut.by/</link>
  6.    <description>Новости TUT.BY</description>
  7.    <item>
  8.      <title>Заголовок новости</title>
  9.      <link>http://news.tut.by/94435.html</link>
  10.      <description>Краткое описание новости</description><pubDate>Mon, 17 Sep 2007 08:40:00 +0300</pubDate>
  11.      <guid>http://news.tut.by/94435.html</guid>
  12.    </item>
  13.   </channel>
  14. </rss>
Здесь корневой тег “rss” содержит тег “channel” играющий роль контейнера для произвольного количества элементов ”item” – новость. Внутреннее устройство новости очевидно: заголовок, собственно текст новости, ссылка на страницу “подробнее” и дата публикации. Последнее свойство guid играет роль уникального идентификатора новости, в примере здесь выбран адрес страницы с текстом новости.

Теперь пример когда javascript, который загружает данную ленту новостей rss и выполняет ее разбор, затем идет конструирование таблицы, каждая строка которой – новость. Еще момент: если мы делаем ajax вызов к содержимому расположенному не в нашем домене, например ваш скрипт размещен на сайте abc.by, а вызов идет к ленте на tut.by, то мы столкнемся с политикой безопасности регламентирующей кроссдоменные вызовы. Так для internet explorer код, который я приведу далее при попытке открытия удаленного адреса приведет к появлению на экране диалогового окна, где мы должны подтвердить что действительно согласны выполнить загрузку данных из другого домена. Этот параметр можно изменить в окне настроек, как показано на рис. 4. Для firefox, opera все гораздо хуже – специальных параметров настроек для управления этим не предусмотрено. В любом случае, можно воспользоваться стандартной методикой обмана: создается файл-посредник. Именно он вызывается из javascript, и в свою очередь выполняет чтение ленты rss с другого сайта (в php ограничения на crossdomain-вызовы помягче). Вот пример такого файла посредника:
  1. <?php
  2.   // важно правильно указать тип заголовков: что именно мы возвращаем
  3.   header('Content-Type: application/rss+xml');
  4.   $f = fopen ('http://news.tut.by/rss/index.rss', 'r') or die ('cannot open file');
  5.   while (!feof($f)) {  
  6.     print fread($f, 8192);  
  7.   }
  8.   fclose($f);
  9. ?>
И, наконец, код, который выполняет чтение новостной ленты и превращает ее в таблицу.
  1. function sender (){
  2.  $.ajax({ 
  3.     type: "POST", 
  4.     url: "fromby.php",
  5.  
  6.     success: function(msg){
  7.        var title = msg.getElementsByTagName ('title') [0];
  8.        title = title.firstChild.nodeValue;
  9.        alert ('Загружена новостная лента: ' + title);
  10.        // получаем список всех новостей в ленте
  11.        var items = msg.getElementsByTagName ('item');
  12.        var t = document.createElement ('table');
  13.        oRow = t.insertRow(0);
  14.        // создаем заголовки таблицы
  15.        oCell = oRow.insertCell(0).innerHTML= 'Заголовок';
  16.        oCell = oRow.insertCell(1).innerHTML= 'Новость';
  17.        oCell = oRow.insertCell(2).innerHTML= 'Ссылка';
  18.        for (var i = 0; i < items.length; i++){
  19.          var rss_item = items[i];
  20.          // извлекаем из документа xml набор тегов
  21.          var i_title = rss_item.getElementsByTagName ('title')[0].firstChild.nodeValue; 
  22.          var i_link = rss_item.getElementsByTagName ('link')[0].firstChild.nodeValue; 
  23.          var i_description = rss_item.getElementsByTagName ('description')[0].firstChild.nodeValue; 
  24.          oRow = t.insertRow(i+1);
  25.          // создаем заголовки таблицы
  26.          oCell = oRow.insertCell(0).innerHTML= i_title;
  27.          oCell = oRow.insertCell(1).innerHTML= i_description;
  28.          oCell = oRow.insertCell(2).innerHTML= i_link;
  29.        }
  30.        document.getElementById('dv_Result').appendChild (t);
  31.      } 
  32.    }
  33.  );
  34.  }// конец ajax вызова
В следующий раз я завершу рассказ об ajax. Нас ждет библиотека hajax и рассказ о том, как можно отправлять на сервер не только простые текстовые данные, но и файлы.