В данной статье описывается библиотека JsHttpRequest (лицензия LGPL), реализующая загрузку данных методом AJAX (Remote Scripting). Вот краткий перечень ее ключевых возможностей и отличий от аналогов:
Не так давно определенную популярность получил новый сервис Google: так называемый Google Suggest. Те, кто еще не видел, что это такое, могут посмотреть прямо сейчас: http://www.google.com/webhp?complete=1&hl=en.
![]() |
Работа Google Suggest заключается в том, что по нескольким введенным буквам специальная программа на JavaScript обращается к сайту Google и запрашивает у него 10 самых "популярных" слов, начинающихся с тех же букв. Скрипт срабатывает настолько быстро, что выпадающий список с вариантами появляется практически мгновенно. Естественно, перезагрузка страницы при этом не производится — все реализовано на JavaScript и DHTML. |
Для реализации "динамической подгрузки" Google использует следующие средства:
Про то, как работает Google Suggest, в Интернете пишут все, кому не лень, и я совершенно не собираюсь повторяться. Вместо этого я представлю другой подход под названием JsHttpRequest, несколько обходящий Google Suggest по совместимости с различными браузерами и гибкости использования.
Метод, который реализует динамическую подгрузку в Google
Suggest, проиллюстрирован ниже на примере загрузки исходного текста
текущей страницы. (Работу с
Листинг 1: test/JsHttpRequest/ActiveX.htm | скопировать код в буфер обмена |
<script> function doLoad() { var req = window.XMLHttpRequest? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); req.onreadystatechange = function() { if (req.readyState == 4) alert('Loaded:\n'+req.responseText); } req.open("GET", document.location, true); req.send(null); } </script> <input type="button" value="Show me" onclick="doLoad()"> |
Этот код будет работать только в Mozilla (Firefox), а также в IE (при включенных ActiveX). Opera 7.x, а также пользователи, выключившие себе ActiveX по соображениям безопасности, "отдыхают".
Чтобы вам не было чересчур скучно, я сразу демонстрирую библиотеку
JsHttpRequest "в действии". Введите несколько слов в текстовом поле
![]() |
Код подгрузки данных с форума для первого примера, пожалуй, слишком сложен. Поэтому далее мы разберем более простой код, специально написанный для облегчения понимания. |
Использовать объект
Приведу пример страницы, которая обеспечивает генерацию хэш-кода MD5 для
введенной пользователем строки. Само вычисление происходит на сервере, а браузер лишь обращается к последнему
за данными, используя объект
Листинг 2: test/JsHttpRequest/smpl_frontend.htm | скопировать код в буфер обмена |
<script src="../../lib/JsHttpRequest/JsHttpRequest.js"></script> <script type="text/javascript" language="JavaScript"> function doLoad(value) { // Create new JsHttpRequest object. var req = new JsHttpRequest(); // Code automatically called on load finishing. req.onreadystatechange = function() { if (req.readyState == 4) { // Write result to page element (_RESULT becomes responseJS). document.getElementById('result').innerHTML = '<b>MD5("'+req.responseJS.q+'")</b> = ' + '"' + req.responseJS.md5 + '"<br> '; // Write debug information too (output becomes responseText). document.getElementById('debug').innerHTML = req.responseText; } } // Prepare request object (automatically choose GET or POST). req.open(null, 'smpl_backend.php', true); // Send data to backend. req.send( { q: value } ); } </script> <form> Text: <input type="text" name="text"> <input type="button" value="Calculate MD5" onclick="doLoad(this.form.text.value)"> </form> <div id="result" style="border:1px solid #000; padding:2px"> Structured results </div> <div id="debug" style="border:1px dashed red; padding:2px"> Debug info </div> |
Если внимательно посмотреть, хорошо видно, что применение JsHttpRequest ничем принципиальным не отличается от использования
Листинг 3: результирующий объект | скопировать код в буфер обмена |
{ q: 'запрос', md5: 'MD5-код введенной строки' } |
В поле
![]() |
Впрочем, ничто не мешает написать загрузчик так, чтобы он передавал основной результат своей работы именно в виде |
Итак, после получения ответа от сервера у объекта
Теперь пришло время посмотреть, как выглядит загрузчик
Листинг 4: test/JsHttpRequest/smpl_backend.php | скопировать код в буфер обмена |
<?php // Load JsHttpRequest backend. require_once "../../lib/JsHttpRequest/JsHttpRequest.php"; // Create main library object. You MUST specify page encoding! $JsHttpRequest =& new JsHttpRequest("windows-1251"); // Store resulting data in $_RESULT array (will appear in req.responseJs). $GLOBALS['_RESULT'] = array( "q" => @$_REQUEST['q'], "md5" => md5(@$_REQUEST['q']), ); // Below is unparsed stream data (will appear in req.responseText). ?> <pre> <b>Request method:</b> <?=$_SERVER['REQUEST_METHOD'] . "\n"?> <b>Loader used:</b> <?=$JsHttpRequest->LOADER . "\n"?> <b>_REQUEST:</b> <?=print_r($_REQUEST, 1)?> </pre> |
Как видите, нам достаточно лишь получить параметры, переданные JavaScript-частью, из стандартных переменных PHP (
Листинг 5: PHP-массив с результатом работы | скопировать код в буфер обмена |
$_RESULT === array( "q" => 'query', "md5" => 'MD5-code of entered string' ) |
Листинг 6: идентичный ему JavaScript-объект | скопировать код в буфер обмена |
req.responseJS === { q: 'query', md5: 'MD5-code of entered string' } |
Библиотека JsHttpRequest поддерживает возможность закачки файлов на
сервер без перезагрузки страницы. Интерфейс для этого используется тот
же самый, только вместо строкового или числового значения,
передаваемого методу
Листинг 7: test/JsHttpRequest/upl_frontend.htm | скопировать код в буфер обмена |
<script src="../../lib/JsHttpRequest/JsHttpRequest.js"></script> <script type="text/javascript" language="JavaScript"> function doLoad(value) { // Create new JsHttpRequest object. var req = new JsHttpRequest(); // Code automatically called on load finishing. req.onreadystatechange = function() { if (req.readyState == 4) { // Write result to page element (_RESULT becomes responseJS). document.getElementById('result').innerHTML = '<b>MD5("'+req.responseJS.q+'")</b> = ' + '"' + req.responseJS.md5 + '"<br> '; // Write debug information too (output becomes responseText). document.getElementById('debug').innerHTML = req.responseText; } } // Prepare request object (automatically choose GET or POST). req.open(null, 'upl_backend.php', true); // Send data to backend. req.send( { q: value } ); } </script> <!-- Please note that we must specify enctype to multipart/form-data! --> <form method="post" enctype="multipart/form-data" onsubmit="return false"> File: <input type="file" name="upl"> <input type="button" value="Calculate MD5" onclick="doLoad(this.form.upl)"> </form> <div id="result" style="border:1px solid #000; padding:2px"> Structured results </div> <div id="debug" style="border:1px dashed red; padding:2px"> Debug info </div> |
См. этот пример в действии: файл upl_frontend.htm. Скрипт-загрузчик
Листинг 8: test/JsHttpRequest/upl_backend.php | скопировать код в буфер обмена |
<?php // Load JsHttpRequest backend. require_once "../../lib/JsHttpRequest/JsHttpRequest.php"; // Create main library object. You MUST specify page encoding! $JsHttpRequest =& new JsHttpRequest("windows-1251"); // Store resulting data in $_RESULT array (will appear in req.responseJs). $GLOBALS['_RESULT'] = array( "q" => 'file ' . $_FILES['q']['name'], "md5" => md5(@file_get_contents($_FILES['q']['tmp_name'])), ); // Below is unparsed stream data (will appear in req.responseText). ?> <pre> <b>Uploaded files:</b> <?=print_r($_FILES, 1)?> </pre> |
Значение атрибута
При организации закачки вам нужно учитывать следующие правила.
В случае нарушения любого из перечисленных выше правил библиотека JsHttpRequest выдаст сообщение об ошибке, которое вы сможете легко обнаружить.
Часто требуется послать на сервер не отдельные значения, а множество
элементов, уже сгруппированных в HTML-форму. Вместо того, чтобы вручную
перечислять все эти элементы в параметре метода
Листинг 9: test/JsHttpRequest/frm_frontend.htm | скопировать код в буфер обмена |
<script src="../../lib/JsHttpRequest/JsHttpRequest.js"></script> <script type="text/javascript" language="JavaScript"> function doLoad(value) { // Create new JsHttpRequest object. var req = new JsHttpRequest(); // Code automatically called on load finishing. req.onreadystatechange = function() { if (req.readyState == 4) { // Write result to page element (_RESULT become responseJS). document.getElementById('result').innerHTML = 'MD5('+req.responseJS.q+') = ' + '"' + req.responseJS.md5 + '"<br> '; // Write debug information too (output become responseText). document.getElementById('debug').innerHTML = req.responseText; } } // Prepare request object (automatically choose GET or POST). req.open(null, 'frm_backend.php', true); // Send data to backend. req.send( { q: value } ); } </script> <!-- Please note that we must specify enctype to multipart/form-data! --> <form method="post" id="f" enctype="multipart/form-data" onsubmit="return false"> Text: <input type="text" name="txt"> File: <input type="file" name="upl"> <input type="button" value="Calculate MD5" onclick="doLoad(document.getElementById('f'))"> </form> <div id="result" style="border:1px solid #000; padding:2px"> Structured results </div> <div id="debug" style="border:1px dashed red; padding:2px"> Debug info </div> |
См. этот пример в действии: файл frm_frontend.htm.
Библиотека JsHttpRequest поддерживает несколько возможностей, которых нет в XMLHttpRequest.
Вы можете включить режим кэширования результата запросов к загрузчику:
Листинг 10: включение кэширования | скопировать код в буфер обмена |
// Enable caching of the query results req.caching = true; |
Теперь, если вы дважды вызовете метод
По умолчанию библиотека JsHttpRequest старается сама
выбрать оптимальный тип загрузчика данных, исходя из возможностей,
поддеживаемых браузером. Например, в Firefox будет практически всегда
использован встроенный объект XMLHttpRequest (но только не в случае,
когда производится закачка файлов: в последнем случае всегда
применяется загрузчик, основанный на FORM и IFRAME). Однако иногда
(например, при написании скриптов тестирования библиотеки) требуется
явно указать, какой загрузчик требуется использовать. Это можно
сделать, присвоив значение свойству
Листинг 11: явное указание загрузчика | скопировать код в буфер обмена |
// Always use FORM loader. req.loader = 'FORM'; |
Наконец, выбор оптимального метода загрузки (GET или POST) можно также поручить библиотеке, указав в первом параметре функции
Приведенные выше примеры показывают, что контекст использования библиотеки JsHttpRequest практически всегда один и тот же. Вначале создается объект библиотеки, затем устанавливаются его свойства и указывается функция, которая будет вызвана при окончании загрузки. Далее осуществляется посылка запроса на сервер.
Чтобы не писать один и тот же код по многу раз, объект
Листинг 12: test/JsHttpRequest/md5_frontend.htm | скопировать код в буфер обмена |
<script src="../../lib/JsHttpRequest/JsHttpRequest.js"></script> <script language="JavaScript"> // Function is called when we need to calculate MD5. function calculate_md5() { JsHttpRequest.query( 'md5_backend.php', // backend { // pass a text value 'str': document.getElementById("mystr").value, // path a file to be uploaded 'upl': document.getElementById("myupl") }, // Function is called when an answer arrives. function(result, errors) { // Write errors to the debug div. document.getElementById("debug").innerHTML = errors; // Write the answer. if (result) { document.getElementById("ans").innerHTML = 'MD5("' + result["str"] + '") = ' + result["md5"]; } }, false // do not disable caching ); } </script> <!-- Please note that we must specify enctype to multipart/form-data! --> <form method="post" enctype="multipart/form-data" onsubmit="return false"> Enter a text: <input type="text" id="mystr"><br> ...or upload a file: <input type="file" id="myupl"><br> <input type="button" value="Calculate MD5" onclick="calculate_md5()"> </form> <div id="ans" style="border:1px solid #000; padding:2px"> Structured results </div> <div id="debug" style="border:1px dashed red; padding:2px"> Debug info </div> |
Как видите, этот пример делает почти то же самое, что и smpl_frontend.htm, однако его код значительно короче и понятнее.
Листинг 13: Использование стандартных функций Prototype | скопировать код в буфер обмена |
<script src="../../lib/JsHttpRequest/JsHttpRequest.js"></script> <script src="../../lib/JsHttpRequest/JsHttpRequest-prototype.js></script> <script language="JavaScript"> new Ajax.Request('your_ajax_script.php', { method: 'get', parameters: { name: 'Дмитрий', file: $("my_upload_file") }, onFailure: function(tr) { alert('Error code: ' + tr.status + '\n'); alert(tr.responseText); }, onSuccess: function(tr) { $("result").innerHTML = tr.responseJS.hello; if (tr.responseText) alert(tr.responseText); } }); </script> |
Конечно, вы можете использовать и любые другие возможности prototype для работы с AJAX (например, функции динамической подгрузки данных в HTML-элемент).
Ключевым отличием библиотеки JsHttpRequest от аналогов является то, что системы, написанные с ее помощью, очень легко отлаживать. Традиционно отладка AJAX-приложений считается достаточно сложной проблемой. Дело в том, что данные поступают от PHP-части в JavaScript-часть в определенном (и весьма строгом) формате, и любое нарушение этого формата (например, из-за ошибки в скрипте-загрузчике) приведет к ошибкам в JavaScript-коде.
Библиотека JsHttpRequest берет на себя всю "грязную
работу" по конвертированию данных из формата, стандартного для PHP, в
формат, принятый в JavaScript. Она перехватывает выходной поток
PHP-скрипта (в том числе — сообщения о синтаксических и других ошибках)
и накапливает его в специальном буфере, чтобы потом передать в свойство
responseText объекта JavaScript. Вы также физически не сможете
заполнить массив
Т.к. все сообщения об ошибках (например, вызов несуществующей функции) PHP печатает прямо в выходной поток (как будто бы через
Вы можете убедиться, что перехват ошибок работает, запустив пример md5_frontend.htm, приведенный выше, и введя в строке текста слово "error". Наверное, Вы уже обратили внимание, что в отладочном блоке выводится сообщение о фатальной ошибке PHP (вызов неопределенной функции), которая привела к немедленному завершению скрипта:
Листинг 14: пример перехваченной фатальной ошибки PHP | скопировать код в буфер обмена |
Fatal error: Call to undefined function error_demonstration__make_a_mistake_calling_undefined_function() in load.php on line 15 |
Ну а чтобы все окончательно прояснилось, приведем примерный результат работы скрипта md5_backend.php, как его "видит" браузер (уже после перевода его библиотекой JsHttpRequest в формат, "понятный" для JavaScript-кода). Результат выглядит так даже в случае возникновения фатальной ошибки.
Листинг 15: пример формата результата | скопировать код в буфер обмена |
JsHttpRequest.dataReady({ "id": "123", // this ID is passed from JavaScript frontend "js": { "str": "строка", "md5": "MD5-код введенной строки" }, "text": "Здесь идут отладочные сообщения и ошибки." }) |
В зависимости от возможностей браузера и особенностей передаваемых данных библиотека JsHttpRequest использует 3 различных способа загрузки данных ("загрузчика").
![]() |
При использовании данного метода в поведении библиотеки JsHttpRequest имеются два отличия от стандартного |
![]() |
Если вас не устраивает встроенный в JsHttpRequest алгоритм
автоматического выбора загрузчика, вы можете явно указать, какой
загрузчик и какой метод (GET или POST) использовать. Это делается при
помощи присваивания значений "SCRIPT", "XML" или "FORM" свойству |
Рассмотрим, как работает библиотека, на примере самого
кроссбраузерного способа загрузки данных. Речь о динамическом создании
и присоединении к текущей странице тэга
![]() |
Конечно, загружаемый скрипт должен выдавать корректный код на JavaScript. Обычный текст таким методом не подгрузишь. О том, чтобы обеспечить корректность JavaScript-кода, полностью заботится библиотека. |
Предположим, что при нажатии на кнопку JavaScript-программа вставляет (c использованием DOM) в текущую страницу следующий тэг:
Листинг 16: обращение к загрузчику | скопировать код в буфер обмена |
<script language="JavaScript" src="load.php?room=303&JsHttpRequest=101-script"></script> |
Что при этом произойдет? Браузер немедленно обратится к серверу со следующим запросом:
Листинг 17: URL запрошенного загрузчика | скопировать код в буфер обмена |
load.php?room=303&JsHttpRequest=101-script |
В результате на сервере запустится скрипт
Листинг 18: пример результата работы загрузчика | скопировать код в буфер обмена |
JsHttpRequest.dataReady({ "id": "101", // this ID is passed from JavaScript frontend "js": [ "Это некоторые данные.", "Они могут иметь произвольную структуру...", { "test": "...и вложенность" } ], "text": "А здесь идет простой отладочный текст." }) |
![]() |
Внимание! Приведенный выше формат результирующих данных - это лишь пример одного из используемых способов передачи данных. Если Вас интересует подробное описание, см. документ Протокол передачи данных. |
Итак, PHP-скрипт
![]() |
М-ммм... "Программа, пишущая другие программы"... "Источник"... Определенно "Matrix has you". |
В итоге код на JavaScript, сгенерированный PHP-скриптом
Ну а уж функция
![]() |
Динамическая генерация тэга |
Библиотека JsHttpRequest состоит из двух частей, работающих совместно друг с другом:
Листинг 19: подключение библиотеки в JavaScript | скопировать код в буфер обмена |
<script language="JavaScript" src="JsHttpRequest/JsHttpRequest.js"> </script> |
Листинг 20: подключение библиотеки в PHP | скопировать код в буфер обмена |
require_once "JsHttpRequest/JsHttpRequest.php"; |
В качестве языка для написания загрузчиков пока что поддерживается только PHP, однако в будущем возможно написание backend-модулей и на других языках. (На самом деле, версии для Perl и C++ уже есть, однако они недостаточно стабильны для того, чтобы включать их в дистрибутив.)
Для уменьшения размера ускорения загрузки основного frontend-модуля библиотеки ее JavaScript-код сжат при помощи утилит в составе пакета Dojo Toolkit. (Здесь Вы можете посмотреть, как он выглядит после сжатия.)
В дистрибутиве библиотеки имеются также две дополнительные директории:
Если Вы взглянете на исходный код frontend-модуля, Вы заметите, что можете самостоятельно скомпоновать необходимый Вам набор загрузочных модулей. Для этого просто удалите кусок кода, хранящий тот или иной загрузчик.
При формировании запроса к загрузчику может потребоваться передать
ему строки, содержащие русские буквы. Естественно, их нельзя напрямую
передавать в URL, а вначале нужно URL-
В JavaScript имеется функция
![]() |
Вообще говоря, в последних версиях JavaScript имеется функция |
К счастью, популярное расширение
![]() |
По многочисленным просьбам, начиная с версии 3.0 библиотека JsHttpRequest может работать и без |
Итак, вы можете вызывать метод
![]() |
Еще раз: если вы хотите использовать библиотеку JsHttpRequest с кодировками, отличными от |
Подведем итоги этой большой статьи. Вначале я перечислю ссылки на программные модули, упоминаемые выше.
Пример использования библиотеки:
Библиотека JsHttpRequest активно используется на форуме forum.dklab.ru. А именно, через нее реализованы следующие функции:
Статьи в Интернете:
Автор библиотеки выражает благодарность Юрию Насретдинову за перевод статьи на английский, а также ценные замечания, касающиеся библиотеки.