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

October 9, 2007

Сегодня мы продолжаем и заканчиваем знакомство с технологией асинхронных вызовов – ajax. В прошлый раз я рассказал о том, что html-страница может подгружать данные с сервера в различных форматах: xml – мы говорим об ajax, в формате json – мы говорим об ajaj. Я рассказал о возможностях, которые представляет библиотека jquery, позволяя нам загружать асинхронно информацию, управлять форматом принимаемых данных, обрабатывать ошибки. Сегодня я смещу фокус рассмотрения материала с того “как бы хотя бы вызвать что-то” к тому “как бы это сделать удобным для пользователя”. Также я расскажу об том, как можно загружать на сервер файлы, как работать с историей браузера.

Начнем мы, однако, с рассмотрения простой и очевидной, но забываемой в бездумной погоне за высокими технологиями проблемы. Окно браузера состоит из множества различных “штуковин”, и самой главной является строка ввода адреса. Пользователь верит, что если он введет в эту строку некоторый адрес, нажмет кнопку ввод, то откроется специальная страница. В принципе, так все и было до того, как начали получать широкое распространение сайты сделанные целиком на flash и ajax. Особенность таких сайтов в том, что после того как flash-ролик был загружен, он реализует собственную логику обработки действий пользователя. Так, когда пользователь жмет на ролике кнопку “о компании”, то ему flash-ролик подгружает содержимое из внешнего ресурса, создает определенный gui, в который помещается информация - но и ни одно из этих действий не отображается в адресной строке браузера. Для ajax-основанных сайтов все то же самое: мы подгружаем внешнее содержимое, вид страницы меняется – но адресная строка остается без изменений. Полгода назад мне попался на глаза сайт сделанный веб-студией Сами_Знаете_Кого, сайт этот представлял некоторый реестр юридической информации, или финансовой – не важно. Главное в том, что информации было много, и она была организована в виде иерархии разделов. Т.е. на странице сбоку было меню в форме дерева, можно было раскрывать узлы – категории документов, и, в конце концов, добраться до собственно хранимых документов. Подгрузка содержимого узлов дерева делалась с помощью ajax, равно как и загрузка содержимого документа в центральную часть страницы. В адресной строке при этом ничего не менялось и представляете себе удовольствие, которое получал безымянный сотрудник, которому нужно было послать по почте ссылку на документ, хранящийся в этом реестре. Вопрос в лоб: можно ли изменить содержимое адресной строки с помощью javascript, flash или как-то еще. Нет, к счастью нет. Знатоки javascript скажут, что можно вызвать метод: “window.location.href = ‘новый_адрес_для_перехода’ ”. Отлично, но какой тут ajax? Если вы при этом покинете страницу, перейдя по новому адресу. Надеяться на то, что адресную строку можно менять без перехода по другому адресу бессмысленно – в Интернете широко распространилось такое явление как “фишинг” – подделка сайта, выдача собственного сайта за другой. Так что, ни один браузер не оставит такую потенциально опасную функцию. Все что нам доступно - это изменять якорь. Ту самую часть адреса, которая расположена после имени документа и отделена от него символом “#”. Соответственно, когда страница загружается, вы внутри javascript-кода страницы должны проверить значение этого якоря, и выполнить загрузку необходимых ресурсов. Аналогично, на каждое действие пользователя загружающее в страницу новую информацию должно изменять значение этого якоря. Например, в примере ниже я создал две страницы php с информацией – они называются contacts.php и documents.php. Также есть главная страница html, содержащая две кнопки, загружающие предыдущие файлы во внутрь тега div. Загрузка идет с помощью метода load, который можно применить к любому узлу документа найденного с помощью вызова $(‘то_что_надо_найти’). В качестве параметров этому вызову я передаю адрес документа для загрузки - некоторый ассоциативный массив с переменными, и функцию, срабатывающую в том случае, если загрузка была успешна. В теле функции я изменяю значение переменной window.location.hash. Когда страница загружена, анализирую чему равна переменная window.location.href и вызываю функции загрузки нужного содержимого. Ниже пример кода главного html-файла:
  1. <script type="text/javascript" src="jquery.js"> </script>
  2.  
  3. <script>
  4. function onLoadDocuments (){
  5.  // здесь выполняется ajax-вызов 
  6.  // а тут мы меняем значение адресной строки
  7.  $("#doc_zone").load("documents.php",
  8.    {year: 2007, month: 1, day: 12},// некоторые параметры нужные для работы php-файла
  9.    function() { 
  10.      window.location.hash = 'documents';
  11.    } 
  12.   );
  13. }
  14.  
  15. function onLoadContacts (){
  16.  // здесь также идет ajax-вызов 
  17.  // а тут мы меняем значение адресной строки
  18.  $("#doc_zone").load("contacts.php",
  19.   {street_no: 125},
  20.   function() { 
  21.     window.location.hash = 'contacts';
  22.   }	 
  23.  );
  24. }
  25.  
  26. $(document).ready(
  27.  function(){ 
  28.    if (window.location.hash == '#documents'){
  29.     // при загрузке страницы смотрим чему равно значение якоря 
  30.     // и выполняем загрузку нужного содержимого
  31.     onLoadDocuments ();
  32.    }
  33.    if (window.location.hash == '#contacts'){
  34.     onLoadContacts ();
  35.    }
  36.  }
  37. );
  38. </script>
  1. <body>
  2.  <div id="doc_zone" style="border: 2px solid black; margin: 10px; padding: 10px;">
  3.   DocZone ...
  4.  </div>
  5.  
  6.  <input type="button" onclick="onLoadDocuments ()" value="load documents"/>
  7.  <input type="button" onclick="onLoadContacts ()" value="load contacts"/>
  8.  
  9. </body>
Результат работы скрипта показан на рис. 1.



На этом про адресную строку все, последнее, о чем упомяну перед новой темой – это как быть, если вы flasher и решили озаботиться поддержкой адресации роликов. Вовсе необязательно изобретать очередной велосипед. Метод работы с window.location.hash, который я вам показал, лег в основу достаточно известной библиотеки swfaddress, ее домашний сайт: http://www.asual.com/swfaddress/

Вторая функция, которой привык пользоваться типовой посетитель сайта: кнопки “назад” и “вперед”. Предполагается, что, нажав “назад”, клиент увидит предыдущую страницу. Увы и опять, ничего подобного не происходит. Вообще до тех пор, пока не будет реализована поддержка ajax-юзабилити на уровне браузера говорить о промышленном внедрении ajax-идей просто бессмысленно. Снова попробуем имитировать отсутствующую функциональность. Неприятность в том, что разные браузеры ведут себя по разному. Так opera и firefox при изменении свойства “window.location.hash” или же “window.location.href” (в этом случае отличия только в части после “#”) сохраняют адрес в историю как НОВЫЙ. Т.е. при нажатии на кнопку “вперед”|”назад” будет меняться значение адресной строки, а физически мы будем оставаться на той же странице. Как отследить момент изменения адресной строки я не нашел. Так попытка установить обработчик события: “window.onunload = то_что_ будет_вызвано_при_ выгрузке_страницы;” и “window.onload = то_что_будет _вызвано_при_загрузке _страницы;” ни к чему не привели. События не генерируются. В объекте history нет никаких событий или свойств позволяющих реагировать на изменении адреса страницы. Максимум, до чего я додумался – это создать функцию, вызываемую по таймеру, которая бы с интервалом, скажем, в 1000 миллисекунд проверяла бы значение адресной строки и вызывала бы соответствующую функцию загрузки содержимого. Например, так:
  1. function testIfAddChanged (){
  2.  // проверяем, какой адрес сейчас текущий и выполняем загрузку нужного содержимого
  3.  if (window.location.hash == '#contacts')
  4.   onLoadContacts ();
  5.  if (window.location.hash == '#documents')
  6.   onLoadDocuments ();
  7. }
  8. // запускаем таймер
  9. window.setInterval ("testIfAddChanged()" , 1000);
Это почти завершенный пример, не хватает только пары проверок позволяющих избежать дублирующиеся загрузки содержимого, но это, я уверен, вы сделаете и сами. Что касается самого “замечательного” в мире браузера internet explorer, то он снова показал свой норов. Изменяй “window.location.hash” или “window.location.href” – ему без разницы - в историю никаких новых записей не вносится. Следовательно, никакой поддержки кнопок “назад”/”вперед” сделать нельзя, хотя я и не проверял как ведет себя седьмая версия ie.

Теперь разберем вопрос загрузки на сервер файлов. Здесь все ужасно плохо. Родной поддержки отправки файлов ajax не имеет. Нам придется имитировать данный процесс с помощью flash или хитроумных хаков. Начнем с вопроса: что может отправить на сервер файл? Очевидно, только форма (тег “form”). Очевидно, что прямой программный доступ к файловой системе из javascript - это страшная дырка в безопасности браузера и ее быть не должно. Итак, форма, но отправка ее приводит к полной перезагрузке страницы – не подходит. Или все же подходит, но если сделать так, что форма будет отправляться не с нашей веб-страницы, а с чего-то другого. Например, внедрим в страницу невидимый плавающий фрейм, содержащий форму, заполним ее информацией и скажем submit и форма отправилась. Или еще проще, форма расположена в главной файле html, а ее свойство target (имя окна в котором должна открыться страница) указывает на тот самый невидимый фрейм.
  1. <form target="upload_frame" action="upload_files.php" enctype=” multipart/form-data”>
  2.  <input type="file" name="upload_file" /><br />
  3.  <input type="submit" />
  4. </form>
  5. <!-- а вот невидимый frame в котором и будет выполняться загрузка данных -->
  6. <iframe name="upload_frame" style="display: none"></iframe>
Можно использовать библиотеку Д. Котерова JsHttpRequest, в ней поддержка отправки форм делается прозрачно. В первой статье серии я уже упоминал об этой библиотеке, но напоминаю, что загрузить ее и прочитать примеры использования можно по адресу: http://dklab.ru/lib/JsHttpRequest/. В следующем примере я создам форму(без нее никак ведь нам нужно отобразить диалог выбора файла, а сделать это без формы или flash не возможно) в форме можно выбрать файл с картинкой, а также указать имя клиента в текстовом поле. После отправки данных на сервер, возвращаются три переменные: одна строковая содержит приветствие, вторая - признак того, что файл удалось успешно загрузить, третья переменная содержит имя файла с картинкой после загрузки на сервер. Это имя используется для установки значения src тега “пустой” картинки расположенной сразу после формы. Общее замечание: важно для формы указать значение onsubmit="return false" – для того чтобы форма не отправилась на самом деле. И вот пример кода на стороне клиента:
  1. <!-- подключаем библиотеку -->
  2. <script src="koterov/lib/JsHttpRequest/JsHttpRequest.js"></script>
  3. <script language="JavaScript">
  4.  function say_hello() {
  5.   JsHttpRequest.query(
  6.   'make_load_img.php', // вызываемый файл
  7.   { // передаем простые текстовые значения
  8.    'username': document.getElementById("username").value, 
  9.    // а также файл картинки для загрузки
  10.    'img_file': document.getElementById("img_file") 
  11.   },
  12.   // Эта функия вызвается когда данные от сервера пришли
  13.   function(result, errors) {
  14.     if (result.file_was_uploaded){
  15.      document.getElementById("img_foto").src = result.nname; 
  16.      document.getElementById("hello_div").innerHTML = result.greetings; 
  17.     }
  18.     else 
  19.       document.getElementById("hello_div").innerHTML = 'Файл не был загружен, ошибка'; 
  20.   },
  21.   false  
  22.   ); 
  23. }
  24. </script>
  1. <form method="post" enctype="multipart/form-data" onsubmit="return false">
  2.  Укажите ваше имя: <input type="text" id="username">
  3.  <br>
  4.  И укажите картинку с вашей фото: <input type="file" id="img_file">
  5.  <br>
  6.  <input type="button" value="Представиться" onclick="say_hello()">
  7. </form>
  8. <div id="hello_div" style="border:4px dotted green;">
  9.    Hello Tag Zone
  10. </div>
  11. <img src="" id="img_foto" />
А вот пример кода на стороне сервера (php-код).
  1. <?php
  2.  require_once "koterov/lib/JsHttpRequest/JsHttpRequest.php";
  3.  // создаем объект JsHttpRequest и указываем кодировку входных данных
  4.  $JsHttpRequest =& new JsHttpRequest("windows-1251");
  5.  // После чего мы можем обращаться к входным данным как к обычным перемнным
  6.  // Результаты работы оформаленные в виде множества переменных 
  7.  // мы помещаем внутрь специального массива _RESULT из библиотеки JsHttpRequest
  8.  
  9.  $status = true;
  10.  // проверяем то был ли успешно загружен файл или нет
  11.  $nname2 = dirname (__FILE__) . '/img/' . $_FILES['img_file']['name'];
  12.  $nname =  'img/' . $_FILES['img_file']['name'];
  13.  if (move_uploaded_file ($_FILES['img_file']['tmp_name'], $nname2)){
  14.   //и если да, то копируем его в папку img - хранилище загружаемых на сервер картинок
  15.   $status = true;
  16.  }
  17.  else  
  18.   $status = false;
  19.  
  20. $GLOBALS['_RESULT'] = array(
  21.  "greetings"   => 'Привет ' . $_REQUEST ['username'],
  22.  "file_was_uploaded"   => $status,
  23.  "nname"   => $nname // возвратим имЯ этого загруженного файла с картинкой на сервере
  24. );
  25. ?>
Результат работы скрипта показан на рис. 2.



Далее, при загрузке файлов интерес представляет задача мониторинга за процессом загрузки: какой процент этой операции уже выполнен, а результаты отображать в виде растущей полоски progressbar. Сразу скажу, что применять методику описанную далее имеет смысл только если размер файла достаточно велик, иначе будет достаточно просто показать некоторую анимированную картинку (например, часы) – мол ждите, загрузка идет. Итак если файл велик и надо следить за тем как она загружается, то … прежде всего эта задача не так и проста и однозначна. Когда вы вызываете некоторый php-скрипт, то он не получает управления до момента полной, я подчеркиваю, полной загрузки всех передаваемых ему данных и файлов. Но все загружаемые файлы помещаются в некоторую временную папку, общую для всех php-скриптов на сервере. Очевидно, что по мере загрузки данных, файл будет расти. Итак файл, растет, затем вызвается php-скрипт, и по окончанию работы скрипта, файл будет удален, так что программисту в общем случае следует позаботиться об его сохранности (в предыдущем примере я использовал для этого функцию move_uploaded_file). Само названии технологии ajax – асинхронные вызовы, говорит нам что можно запустить один процесс, который будет загружать файл, а также с некоторым интервалом запускать процесс, который будет отслеживать размер растущего временного файла и возвращать это число в код javascript, там мы его уже можем использовать в роли progressbar. Увы, увы, это будет работать только в идеальной ситуации. Например, у вас на локальном сервере, или даже в Интернете, если нагрузка на сервер не велика. Проблема в том, как узнать какой файл является временным? Хорошо если ваш хостинг (виртуальный – наиболее типичный и дешевый вариант хостинга – предполагает что на одном физическом компьютере выполняется несколько веб-сайтов) настроен таким образом, что у каждого сайта (клиента) собственная папка для временных файлов. Но даже если это так, то вам нужно надеяться только на то, что в одно и тоже время не будет запущен еще один (любой скрипт) загружающий на сервер файлы. Например, по адресу http://www.sql.ru/forum/actualthread.aspx?tid=299200, находится подобное почти рабочее решение - рискуйте. Лучший вариант - использовать flash. Например, библиотека swfupload, ее можно загрузить на сайте http://swfupload.mammon.se/. Вот пример кода с комментариями:
  1. <script type="text/javascript" src="SWFUpload.js"></script>
  2. <script type="text/javascript">
  3.  var swfu;
  4.  
  5.  window.onload = function() {
  6.   // Создаем объект-загрузчик
  7.   swfu = new SWFUpload({
  8.   target:"zed",
  9.   // здесь указывается блок div внутрь которого будут помещены кнопки выбора файла и кнопки отправки
  10.   create_ui : true,// создать ли эти кнопки автоматически
  11.   upload_script : "upload.php",// файл которому будет передан файл
  12.   flash_path : "SWFUpload.swf",// имя к flash¬-части библиотеки
  13.   upload_progress_callback : 'uploadProgressFunction',// фукнция вызываемя во время загрузки
  14.   flash_loaded_callback : "swfu.flashLoaded"			
  15.   }
  16.  );
  17. }
  18.  
  19. function uploadProgressFunction(file, bytesloaded, bytestotal) {
  20.  var progress = document.getElementById("yourprogressid");
  21.  // нам передается размер файла, и количество уже выгруженных на сервера байт
  22.  var percent = Math.ceil((bytesloaded / bytestotal) * 100);
  23.  progress.innerHTML = percent + "%";}
  24.  // кусок html куда будут выводиться результаты работы
  25. </script>
  26.  
  27. <div id="zed"></div>
  28. <div id="yourprogressid"> %</div>
Результат работы скрипта показан на рис. 3.



Закончить эту статью не упомянув пару слов про отладку ajax-основанных решений не возможно. Я рекомендую использовать браузер firefox с установленным плагином firebug (возможно применять и livehttpheaders). Затем, открыв окно firebug (меню Tools->Firebug->Open firebug), вы переходите на закладку “Net” и видите перечисление всех асинхронных запросов, которые были сделаны со страницы, также видите какие данные вы отправили на сервер и которые получили, видите и значения служебных заголовков. Пример окна firebug показан на рис. 4.



Я снова не успел рассказать об xajax – библиотеке создающей некоторый слой-посредник сглаживающий различия между javascript-кодом и серверным кодом (php, asp.net, …) – наверное это и к лучшему. Я решил, что стоит собрать больше материала и написать отдельную серию статей посвященных интересным для php cmf-ам – наборам функций, библиотекам, методикам написания кода. Там найдется место и для xajax и symfony и cakphp. Ждите.