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

September 24, 2007

Сегодня я начну серию материалов посвященных технологии ajax – средстве позволяющем строить сложные веб-приложения и отойти от традиционной модели “на каждое действие пользователя - загрузка новой страницы”. Расшифровывается ajax как asynchronous javascript and xml. Широко о методах ajax заговорили года два назад, однако первые технические средства позволяющие подзагружать в веб-страницу данные с сервера появились еще раньше - лет восемь назад. Хотя широкого применения ajax не нашел. С одной стороны причиной этому было то, что поддержка ajax была реализована только в internet explorer и, причем, в виде activex компонента – потенциальная опасность этой технологии сразу отпугнула ряд клиентов и администраторов их сетей. И самое главное - тогда еще не сформировалась потребность создания приложений в стиле RIA (rich internet application). Лишь с выходом gmail и еще пары известных сайтов, веб-интерфейс которых был построен на ajax и имитировал поведение традиционного desktop-приложения, посетители сети сказали, что им нравятся возможности, которые дают ajax. И восьмилетний застой сменился бумом, в короткое время появилось множество javascript библиотек, должны были бы автоматизировать типовые операции ajax (делали их все кому не лень и серьезные компании и программисты-энтузиасты). Издалось множество книг (большей частью все, что я читал по этой теме, было преисполнено пустой болтовни о великих перспективах – конкретики очень мало, и я дальше скажу почему). Начали говорить о новом поколении в истории internet. Пришло время рассмотреть ajax более критично.

В чем идея ajax? Самые первые сайты (я говорю о временах начала девяностых) были реализованы в виде статических html-страниц, генерировались ли они некоторым скриптом на основании данных хранящихся в базе данных или нет – не столь важно. Главное, что после того как клиент получал страницу на своем экране, то единственное, что он мог совершить - это нажать на какую-нибудь ссылку или кнопку, чтобы эта страница (вся страница целиком) сменилась чем-то иным. Потом появился dhtml – динамический html. Вся его динамичность заключалась в том, что кроме текста html в страницу мог быть внедрен некоторый javascript-код (который уже не занимался баловством вроде вывода вверху страницы текущей даты или времени), а изменял узлы дерева DOM. Что это за дерево и зачем оно нужно? DOM называют одним из столпов на которых построен dhtml, вместе с css и javascript. DOM – способ представления всего содержимого страницы в виде интерактивного дерева, корень этого дерева тег “html”, из которого произрастают ветви тегов “body”, “head”. Из них в свою очередь растут теги “p”, “div”, “table” и другие. Внутри тегов находится собственно информационное наполнение – текст страницы. Дерево DOM интерактивно и это значит, что если вы программно из javascript изменяете некоторый узел, например, изменить у тега “p” свойство css color, то текст находящийся внутри абзаца “p” изменит свой цвет. Некоторое время dhtml использовали для украшательства страниц: раскрывающиеся менюшки, картинки, при наведении мыши на которые меняется их размер, ну и мало ли чего можно придумать. Одним словом мы меняли внешний вид страниц. А как насчет данных находящихся внутри страницы, можно ли динамически их менять? Да можно, dhtml это вполне позволял, так у каждого тега есть атрибут innerHTML, содержимое его задает то, что находится внутри этого тега. Если вы условно присвоите “p.innerHTML = ‘привет’; ”, то содержимое абзаца текста изменится. Казалось бы, вот теперь можно строить по настоящему динамические веб-страницы, содержимое которых способно меняться без того чтобы перезагружать страницу с нуля. Увы, для того чтобы присвоить свойству innerHTML новое значение, необходимо знать это самое значение, т.е. оно должно было уже присутствовать внутри страницы, пусть и, не показываясь до некоторого момента. В тривиальных случаях действительно можно было закачать внутрь документа html весь этот псевдо-динамический текст. Например, если вы делаете на странице выпадающее меню с перечислением товаров в каталоге, то можно было бы все его содержимое поместить внутрь документа в виде невидимых тегов, или переменных javascript – что не суть важно. Затем эти данные по клику на раздел каталога помещались в соответствующие теги с innerHTML. Вот вам и динамика – страница не перезагружается, но в центре ее показывается новая информация. Увы, если каталог велик, или его содержимое способно меняться часто (скажем, система заказа билетов, результаты голосований), то фокус не пройдет. Данные нужно загружать по мере необходимости. Выбрал клиент раздел каталога “мебель” – загрузили только список товаров заданного типа. Клиенту удобно – он не ждет лишнее время, пока загрузится огромная страница с невидимыми и ненужными ему товарами. И таких примеров можно придумать множество.

Теперь собственно к делу. Данные, которые подзагружаются в страницу, должны генерироваться некоторым скриптом (php, asp.net – не важно), и почти всегда этому скрипту нужно передать входные данные – да хоть тоже название раздела каталога, которое выбрал клиент в динамическом меню. Итак, нам нужен способ передать скрипту данные – сложные данные: массивы, обычные и ассоциативные. Далее нам нужен способ вернуть отобранные из базы сведения клиенту. Вопрос, в каком формате эти данные вернуть? Вариант 1 – просто текст html – тоже содержимое раздела каталога, которое, казалось бы, можно сразу брать и помещать внутрь тега “p.innerHTML”. Увы, это самый простой и самый плохой вариант – его минусы в том, что мы передаем по сети избыточный трафик (теги html служащие для визуализации – зачастую их больше чем собственно текста). Но скорость работы это еще ладно, важнее другое: крайне редко бывает так, что информация, которая приходит клиенту атомарная. Здесь под атомарностью я понимаю единость и неделимость информации. В действительности же, сервер может вернуть вам сообщение об ошибке: мол, неправильное название каталога или билетов на выбранную дату нет. Кодов ошибок может быть множество, также возможно, что информация должна быть помещена в несколько разных мест страницы. Так, при выборе раздела каталога, отображаем его содержимое в центральной части страницы, а где-нибудь вверху документа покажем баннер со спец-предложением (естественно для вот этого, выбранного, раздела каталога). Как вы планируете разделить возвращенный сервером монолитный кусок html на несколько частей, выделить среди них коды ошибок? Можно поиграть с несколькими асинхронными вызовами, каждый из которых загружает свою часть страницы, но … Стоп, а что такое асинхронный вызов? Это значит то, что действие – загрузка некоторой информации – идут параллельно друг с другом, а также с тем, как клиент, дожидаясь завершения своего действия, – загрузки содержимого каталога – лениво водит мышью по странице и жмет, на другой раздел меню. Здесь я непроизвольно затронул сложнейшую проблему – скорость загрузки информации не мгновенна, и время, которое будет затрачено на обработку первого запроса может быть больше чем на второй запрос. Учитывая, что мы разбили запрос на два или более параллельных запроса (один из которых грузит центральную часть страницы, второй – баннер спец-предложения), то наша программа становится похожей на вавилонскую башню. Кто-то стоит фундамент, кто-то возводит стены, а кто-то уже кладет крышу и клеит обои на стены, которых еще толком нет. Ладно, как вывод, сервер не может вернуть в браузер просто текст html – это не гибко и не надежно. Если не html, то кто? На роль кандидата сразу же выпрыгнул xml. Тот самый гибкий, расширяемый язык разметки, идеально подходящий под роль универсального формата для переноса информации. Собственно, четвертая буква аббревиатуры ajax – “x” – и означает xml. Именно в формате xml данные передаются из страницы на сервер и в этом же формате данные с сервера возвращаются клиенту. Для того чтобы выполнить разбор возвращенного xml можно использовать специальные функции, благо поддержка xml есть в любом браузере.

К слову о браузерах, то, что internet explorer имел поддержку ajax (правда, с помощью внешнего activex-компонента) все слышали еще лет восемь назад. А как насчет firefox и opera. С какой версии firefox получил поддержку ajax я не скажу, но точно помню, что весной 2005 г. она уже была. Opera же получила данную функцию только с выходом 9-ой версии. Так что пару лет назад разработка вспомогательных javascript библиотек для работы с ajax велась в основном в направлении “лишь бы это работало во всех браузерах”. Даже с internet explorer-ом не все было так гладко: поддержка ajax с помощью activex порождала множество проблем. Прежде всего политика безопасности. В настойках браузера можно управлять тем как будут себя вести activex компоненты, будут ли они доступны, запрещены или решение об их запуске будет принимать сам клиент, отвечая на некоторый запрос браузера “разрешить выполнить вызов ajax? Да. Нет”. Неправда ли, это очень удобно? В вышедшем недавно internet explorer 7 поддержка ajax, наконец-то, стала “родной” – свершилось, все новые версии браузеров поддерживают ajax, а как быть со старыми? Тот период породил несколько интересных продуктов имитировавших ajax с помощью методики “в странице есть много такого, что способно загружаться без ее полной перезагрузки”. Например, для этой роли использовались невидимые плавающие фреймы. С помощью javascript можно было заставить их загрузить внутрь себя некоторую служебную страницу с данными от сервера. Потом додумались использовать невидимые картинки. Самый лучший способ выбрал Дмитрий Котеров (известный писатель учебников по php, сотрудник yandex и прочая и прочая …). Был ли Котеров первым неизвестно, но, по крайней мере, его библиотека выглядит готовой к использованию и имеет хорошую поддержку на сайте автора http://dklab.ru. Он предложил динамически менять свойство “src” у тега “script”. Тег “script” выглядит либо так:
  1. <script type="text/javascript">
  2.   var x = 1;// объявляем переменные
  3.   alert (x);// и еще много-много действий
  4.  </script>
В этом случае внутри тега размещается код: функции переменные. И с ними ничего поделать нельзя. А вот такой пример позволял выполнить загрузку содержимого js-файла извне.
  1. <script type="text/javascript" src=”js/functions.js"> </script>
Следовательно, если изменить значение данного атрибута “src” и указать адрес не статической javascript-страницы, а адрес динамического документа генерируемого серверным скриптом, то браузер выполнит его загрузку. А в нем будут находиться некоторые js-переменные, значения которых мы используем для динамической смены содержимого самого документа. Звучит громоздко, но давайте разберем это на примере. Я создам страницу, которая содержит текстовое поле, содержимое которого будет отправлено на сервер в файл php. Его же цель вернуть эту строку перевернутой (реверсированной), в случае если размер строки менее чем 5 символов, то возвращается сообщение об ошибке.

Файл html с текстовым полем, кнопкой, и функцией javascript делающей собственно асинхронный вызов.
  1. <html>
  2.  <head>
  3.  
  4.  // блок javascript с идентификатором, именно его мы будем применять для вызова
  5.  <script id="js" type="text/javascript"> </script>
  6.  
  7.  <script type="text/javascript" language="javascript">
  8.  // функция, вызываемая всякий раз когда скрипт php отработает и вернет данные браузеру
  9.  function makeLoadComplete (){
  10.   var dv_Result = document.getElementById ('dv_Result');
  11.   // проверяем код завершения переворота строки
  12.   if (js_ErrCode){
  13.     // если ошибки, то выводим текст сообщения об ошибке 
  14.     dv_Result.innerHTML = js_ErrMsg;
  15.     dv_Result.style.color = 'red'; 
  16.   }
  17.   else{  // и если все удачно то выводим результат в подготовленный блок div
  18.     dv_Result.innerHTML = js_Result;
  19.     dv_Result.style.color = 'green';
  20.   } 
  21.  }
  22.  
  23.  function sender (){
  24.   var js = document.getElementById ('js');
  25.   var str_for_rev = document.getElementById ('str_for_rev');
  26.   // и отправляем запрос
  27.   js.src = 'makereverse.php?text=' + str_for_rev.value; 
  28.  }
  29.  </script>
  30. </head>
  31.  
  32. <body>
  33.  <!—блок для вывода результат переворачивания -->
  34.  <div id="dv_Result" style="">Result ...</div>
  35.  Введите в поле некоторый текст:
  36.  <input type="text" id="str_for_rev" value="text value" /> <br />
  37.  И нажмите на кнопку, чтобы выполнить его переворачивание:
  38.  <input type="button" onclick="sender ()" value="click me !"/>
  39. </body>
  40. </html>
Теперь код файла php. Обратите внимание на его последнюю строчку. Дело в том, что мы не можем из вызывающего js-кода узнать когда именно данные были загружены. Для локального сервера это может быть мгновенно, но в Интернете, увы, нет. Для тега script нет обработчика события onload, поэтому я явно вызвал функцию makeLoadComplete.
  1. <?php
  2.  // очень важно указать, что тип возвращемого документа javascript
  3.  header ('Content-Type: text/javascript');
  4.  // всегда-всегда перед обработкой данных проверяйе что они вообще были переданы внутрь скрипта
  5.  $text = isset($_REQUEST ['text'])?$_REQUEST ['text']:'';
  6.  
  7.  if (strlen($text)< 5){
  8.   print 'var js_ErrCode = true;' . "\n";
  9.   print 'var js_ErrMsg = "Увы ошибка, не переворачиваю";' . "\n"; 
  10.  }
  11.  else{
  12.   print 'var js_ErrCode = false;' . "\n";
  13.   print 'var js_ErrMsg = "Ошибок нет, переворачиваю";' . "\n"; 
  14.  }
  15.  
  16.  print 'var js_Result = "'.strrev ($text) .'"; ' . "\n";
  17.  print 'makeLoadComplete ();' . "\n";
  18. ?>


Осторожно: не применяйте этот код в коммерческом приложении. Мало того, что он не работает под firefox, так это всего лишь прототип, позволяющий вам понять методику асинхронных вызовов. Здесь не хватает средств обработки множественных одновременных запросов, обработки ошибок загрузки страницы, кэширования, средств отладки, удобной передачи нескольких переменных, содержимого форм, поддержки Unicode и многого другого. Все это можно найти в библиотеке JsHttpRequest от Д. Котерова или читайте дальше – я расскажу об иных библиотеках и подходах к ajax.

Если ориентироваться на новые браузеры, то следует использовать объект XMLHttpRequest. Именно он служит для выполнения собственно загрузки данных, плюс предоставляет множество свойств управляющих тем, как это происходит. Их я кратко перечислил в таблице ниже:
Функция/свойство Описание
Onreadystatechange Это функция вызывается всякий раз, когда изменяется значение свойства readyState.
readyState Свойство, хранящее статус или название текущего этапа в работе XMLHttpRequest. Возможные значения: 0 – не инициализирован. 1 – объект подготовлен для удаленного вызова, но сам вызов еще не был выполнен. 2 – вызов был сделан и в браузер даже пришли заголовки ответа с сервера, но данных еще нет – ждем. 3 – уже загрузилась часть собственно данных, но еще не все. 4 – данные полностью были загружены и подготовлены к работе.
responseText Ответ котор7ый сформировал сервер, в виде обыкновенного текста.
responseXML Ответ сервера, но уже в виде XML-документа. Вы можете вызывать специальные методы этого документа-объекта. Если же данные, которые пришли от сервера не могут быть представлены как xml, то значение свойства responseXML будет равно NULL.
Status Статус ответа. Код 200, 403, 404, 500 – этими кодами сервер кодирует, был ли найден запрошенный файл, хватило ли прав для доступа к нему, не возникла ли при обработке запроса ошибка и т.д.
Abort Функция прерывающая выполнение запроса.
open(method, url, is_asynch) Подготовка к запросу файла uri методом (GET или POST). И признак того будет ли вызов собственно асинхронным или нет.
send(user_data) Отправка запроса с набором пользовательских данных user_data
setHeader(h,v) Перед отправой запроса вы можете установить значения специальных заголовков. Особой необходимости в этом нет: и cookies и session передаются без проблем.
Теперь пример кода, который использует XMLHttpRequest. В нем делается вызов к статическому файлу

ax01.xml со следующим содержимым:
  1. <?xml version="1.0" encoding="windows-1251"?>
  2. <result>
  3.    <color>red</color>
  4.    <message>Запрос выполнен успешно</message>
  5. </result>
Эти сведения загружаются, затем значение элемента color используется для изменения цвета некоторого элемента div, а содержимое элемента message будет помещено внутрь блока.
  1. var requestObj = null;
  2.  if (window.XMLHttpRequest) {
  3.   //это работает для opera и firefox
  4.   requestObj = new XMLHttpRequest();
  5.  } 
  6.  else 
  7.    if (window.ActiveXObject) {
  8.      // а это проверка internet explorer
  9.      requestObj = new ActiveXObject("Msxml2.XMLHTTP");
  10.      if (!requestObj)
  11.       requestObj = new ActiveXObject("Microsoft.XMLHTTP"); 
  12.    };
  13.  
  14.  function sender (){
  15.   if (! requestObj ) return;
  16.   requestObj.onreadystatechange = function (){
  17.     if (requestObj.readyState == 4 ){
  18.       if (requestObj.status == 200){
  19.         var dv_Result = document.getElementById ('dv_Result');
  20.         var result = requestObj.responseXML.getElementsByTagName('result')[0];
  21.         dv_Result.style.color =result.getElementsByTagName('color')[0].firstChild.nodeValue;
  22.         dv_Result.innerHTML = result.getElementsByTagName('message')[0].firstChild.nodeValue;  
  23.       }
  24.       else  
  25.         alert ('Ошибка, запрос не может быть выполнен, код: ' + requestObj.status); 
  26.     } 
  27.   };
  28.   requestObj.open('POST','ax01.xml',true);
  29.   requestObj.send(); 
  30.  }
В следующий раз я продолжу разговор об ajax. Мы рассмотрим, как может быть выполнено формирование файла xml с ответом сервера. Также поговорим о технологии ajaj и нескольких библиотеках автоматизации связывания javascript – php.