Subsys_JsHttpRequest: подкачка данных без перезагрузки страницы (AJAX)
Не
так давно определенную популярность получил новый сервис Google: так
называемый Google Suggest. Те, кто еще не видел, что это такое, могут
посмотреть прямо сейчас: http://www.google.com/webhp?complete=1&hl=en. Работа Google Suggest заключается в том, что по нескольким введенным буквам специальная программа на JavaScript обращается к сайту Google и запрашивает у него 10 самых «популярных» слов, начинающихся с тех же букв. Скрипт срабатывает настолько быстро, что выпадающий список с вариантами появляется практически мгновенно. Естественно, перезагрузка страницы при этом не производится — все реализовано на JavaScript и DHTML. Для реализации «динамической подгрузки» Google использует следующие средства: 1. В Internet Explorer: ActiveX-компонента с именем Msxml2.XMLHTTP или Microsoft.XMLHTTP. 2. В Mozilla и FireFox: встроенный класс XMLHttpRequest. 3. В Opera: динамически создаваемый нулевого размера (скрытый). Про то, как работает Google Suggest, в Интернете пишут все, кому не лень, и я совершенно не собираюсь повторяться. Вместо этого я представлю новый подход под названием Subsys_JsHttpRequest, обходящий Google Suggest по совместимости с различными браузерами. Недостатки подхода Google SuggestИтак, в разных браузерах Google применяет совершенно различные методы подгрузки. Рассмотрим их недостатки. 1. Т.к. в IE используется ActiveX-компонента, вы должны включить ActiveX в настройках браузера. И хотя по умолчанию данная функция как раз включена, многие пользователи, наслышанные о многочисленных дырах IE, вручную ее отключают. Лирическое отступлениеНесмотря на это, вы все же можете убедиться, что Google Suggest продолжает работать и после выключения ActiveX. Видимо, задействуется механизм, основанный на (как в Opera). В любом случае, на ваших сайтах Microsoft.XMLHTTP при выключенных ActiveX работать не будет (это проверено). Про недостатки -метода сказано ниже. 2. Класс XMLHttpRequest, используемый в Mozilla и FireFox, в настоящий момент присутствует только в этих браузерах (поддержка этого класса в Opera 8.01 весьма ограничена). У него есть небольшой недостаток: при умолчательных настройках FireFox запрещено загружать данные откуда-то, кроме как с текущего сайта. 3. Применение динамически создаваемого связано с массой проблем. Главный недостаток — при изменении атрибута src у раздается характерный щелчок и добавляется запись в «историю браузера», так что кнопка Back (Назад) начинает работать неправильно. И хотя данный «подводный камень» можно обойти (весьма искусственным способом), возникают новые проблемы, различные в разных браузерах. Я не буду их сейчас перечислять; скажу только, что за 2 дня перепробовал множество (штук 20) всевозможных вариантов, но добиться кроссбраузерного кода, работающего одинаково и без посторонних эффектов во всех браузерах, мне так и не удалось. Другой недостаток — большой расход памяти и медлительность: фактически, для каждого фрейма создается новый отдельный браузер, который независимо обрабатывает загруженный HTML-код. Короче говоря, Google использует разные (ортогональные, несовместимые) подходы в различных браузерах. Метод, который реализует динамическую подгрузку в Google Suggest, проиллюстрирован ниже на примере загрузки исходного текста текущей страницы. (Работу с я здесь не привожу, потому что она довольно сложна. Речь идет только о классе XMLHttpRequest и ActiveX-компоненте Microsoft.XMLHTTP). <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 по соображениям безопасности, «отдыхают». Принцип работы Subsys_JsHttpRequestМногочисленных проблем и особенностей с ActiveX, XMLHttpRequest и можно избежать, если... не использовать данные технологии. Это звучит действительно банально, однако — работает! Дело в том, что существует один прекрасный и более-менее кроссбраузерный способ загрузки данных на страницу. Очень странно, что разработчики Google до него не догадались. Речь о динамическом создании и присоединении к текущей странице тэга . Такому тэгу следует указать атрибут src, совпадающий с адресом серверного скрипта подгрузки данных (написанного, к примеру, на PHP). ЧайникКонечно, загружаемый скрипт должен выдавать корректный код на JavaScript. Обычный текст таким методом не подгрузишь. Рассмотрим на примере, как работает данный подход. Предположим, что при нажатии на кнопку JavaScript-программа вставляет (c использованием DHTML) в текущую страницу следующий тэг: <script language="JavaScript"
src="load.php?ff=ok&opera=sucks&123"></script> Что при этом произойдет? Браузер немедленно обратится к серверу со следующим запросом: Листинг 3 скопировать код в буфер обмена load.php?ff=ok&opera=sucks&123
В результате на сервере запустится скрипт load.php, который получит в QUERY_STRING параметры ff=ok&opera=sucks&123 (конечно, аргументы могут быть произвольными). Программа отработает (к примеру, обратится к базе данных) и напечатает в качестве результирующей страницы следующий текст: Листинг 4 скопировать код в буфер обмена Subsys_JsHttpRequest_Js.dataReady(
123, [ 'Это некоторые данные.', 'Они могут иметь произвольную структуру...', { test: '...и вложенность' } ], 'А здесь идет простой отладочный текст.' ) Если вы не поняли, к чему все эти квадратные и фигурные скобки, скорее прочитайте тридцать восьмую наблу, в которой рассматриваются особенности синтаксиса JavaScript. Итак, PHP-скрипт load.php напечатал в свой выходной поток текст, являющийся по совместительству корректной JavaScript-программой. Он будет использован браузером в качестве источника данных произвольной структуры. М-ммм... «Программа, пишущая другие программы»... «Источник»... Определенно «Matrix has you». В итоге код на JavaScript, сгенерированный PHP-скриптом load.php, будет выполнен браузером! Как видите, вызывается метод dataReady() объекта Subsys_JsHttpRequest_Js, которому передается: 1. Уникальный идентификатор загрузки (чтобы не спутать одни данные с другими, ведь страница может одновременно запросить сведения сразу из нескольких источников). 2. Произвольные данные, полученные программой load.php, например, из БД. 3. Некоторый текст, который может быть использован в отладочных целях (например, там удобно указывать сообщения об ошибках, возникших в PHP-программе). Ну а уж функция Subsys_JsHttpRequest_Js.dataReady() заботится о доставке загруженых данных конечному потребителю, осуществляя также кэширование одинаковых запросов (если это разрешено). Динамическая генерация тэга <SCRIPT> имеет одно важное достоинство: при использовании такого подхода «история» браузера (history) не засоряется лишними ссылками, а при загрузке не слышно щелчка, издаваемого многими браузерами во время перехода на другую страницу. Нужно также заметить, что в FireFox имеется небольшая ошибка, в результате которой статус-строка не очищается после загрузки <SCRIPT>-компонента (в ней остается сообщение "Loading ..."). Впрочем, эта ошибка ни на что не влияет и, вероятно, будет в скором времени исправлена разработчиками. Библиотека Subsys_JsHttpRequest состоит из двух частей, работающих совместно друг с другом: * Subsys/JsHttpRequest/Js.js, 8 КБ: JavaScript-код, определяющий класс-объект Subsys_JsHttpRequest_Js. Это — так называемый frontend системы («передний проход»). Его следует подключать к страницам с помощью тэга: Листинг 5 скопировать код в буфер обмена <script language="JavaScript" src="Subsys/JsHttpRequest/Js.js">
</script> * Subsys/JsHttpRequest/Php.php, 10 КБ: PHP-код, в котором определяются функции для облегчения написания загрузчиков на PHP. Это — так называемый backend системы («задний проход»). Его следует включать в самое начало программы оператором: Листинг 6 скопировать код в буфер обмена require_once "Subsys/JsHttpRequest/Php.php";
В качестве языка для написания загрузчиков выбран PHP, потому что он: * Весьма распространен. * Крайне быстр, если приходится работать с маленькими скриптами, коими как раз и являются загрузчики. (Это, естественно, касается только mod_php — он так чаще всего и ставится хостерами.) * В большинстве случаев имеет встроенную поддержку Unicode (расширение iconv), которая, как вы увидите ниже, нам очень понадобится. Можно, конечно, писать скрипты загрузки и на CGI-perl, однако в этом случае нагрузка на сервер резко возрастет, что для динамической подгрузки данных противопоказано. Ну а mod_perl встретишь далеко не на каждом хостинге. Класс Subsys_JsHttpRequest_Js: frontend Использовать объект Subsys_JsHttpRequest_Js в JavaScript-программе совсем просто. Собственно, его интерфейс практически не отличается от интерфейсов FireFox-овского XMLHttpRequest или IE-шного Microsoft.XMLHTTP (он специально так разрабатывался). Приведу пример страницы, которая обеспечивает генерацию хэш-кода MD5 для введенной пользователем строки. Само вычисление происходит на сервере, а браузер лишь обращается к последнему за данными, используя объект Subsys_JsHttpRequest_Js. (Этот же пример в действии.) Листинг 7 скопировать код в буфер обмена php
// Проверка работы с сессиями. session_start(); $_SESSION['hello'] = 'Backend loaded at '.date('r'); ?> <html> <head></head> <body> <script language="JavaScript" src="../../../lib/Subsys/JsHttpRequest/Js.js"></script> <script> // Вызывается по тайм-ауту или при щелчке на кнопке. function doLoad(force) { // Получаем текст запроса из <input>-поля. var query = '' + document.getElementById('query').value; // Создаем новый объект JSHttpRequest. var req = new Subsys_JsHttpRequest_Js(); // Код, АВТОМАТИЧЕСКИ вызываемый при окончании загрузки. req.onreadystatechange = function() { if (req.readyState == 4) { if (req.responseJS) { // Записываем в <div> результат работы. document.getElementById('result').innerHTML = 'MD5("'+(req.responseJS.q||'')+'") = ' + '"' + (req.responseJS.md5||'') + '"<br> ' + 'Session data: ' + '"' + (req.responseJS.hello || 'unknown') + '"'; } // Отладочная информация. document.getElementById('debug').innerHTML = req.responseText; } } // Разрешаем кэширование (чтобы при одинаковых запросах // не обращаться к серверу несколько раз). req.caching = true; // Подготваливаем объект. req.open('POST', 'load.php?test=abc', true); // Посылаем данные запроса (задаются в виде хэша). req.send({ q: query, test:303 }); } // Поддержка загрузки данных по тайм-ауту (1 секунда после // последнего отпускания клавиши в текстовом поле). var timeout = null; function doLoadUp() { if (timeout) clearTimeout(timeout); timeout = setTimeout(doLoad, 1000); } </script> <!-- Форма --> <a href="=$_SERVER['REQUEST_URI']?>">Reload myself</a> <form onsubmit="return false"> <input type="text" id="query" onkeyup="doLoadUp()"> <input type="button" onclick="doLoad(true)" value="load"> <br><i>Введите "error", чтобы протестировать отладочные возможности библиотеки.</i> </form> <!-- Результаты работы (заполняется динамически) --> <div id="result" style="border:1px solid #000; margin:2px"> Results </div> <!-- Отладочная информация (заполняется динамически) --> <div id="debug" style="border:1px dashed red; margin:2px"> Debug info </div> </body> </html> <hr> (__FILE__)?> Из-за обилия комментариев выглядит страшно, однако, если внимательно посмотреть, хорошо видно, что применение Subsys_JsHttpRequest_Js ничем принципиальным не отличается от использования XMLHttpRequest или Microsoft.XMLHTTP. Имеется одна важная особенность библиотеки: результат работы load.php удобно получать из свойства req.responseJS. Как видно, в него загрузчик помещает следующий хэш: Листинг 8 скопировать код в буфер обмена {
q: 'запрос', md5: 'MD5-код введенной строки' } В поле req.responseText хранятся данные, выданные скриптом load.php в свой выходной поток (операторами echo). В большинстве случаев они содержат лишь сообщения об ошибках (если ошибки имели место), и именно поэтому данное свойство трактуется как отладочное. Лирическое отступление Впрочем, ничто не мешает написать загрузчик так, чтобы он передавал основной результат своей работы именно в виде req.responseText (хотя это и не очень удобно — см. ниже). Класс Subsys_JsHttpRequest_Php: backendТеперь пришло время посмотреть, как выглядит загрузчик load.php. Помните, мы говорили, что результатом его работы должен быть текст, являющийся корректным JavaScript-кодом. Т.е. просто вывести "Привет, это сгенерированные данные!" нельзя — нужно вначале «обернуть» их вызовом функции Subsys_JsHttpRequest_Js.dataReady(). Вы можете подумать: сколько же мороки возникает из-за этого... Ведь достаточно допустить одну маленькую ошибку (к примеру, пропустить запятую), как результирующий код перестанет быть корректным, в то время как загрузчик не выдаст никакой ошибки. Но взгляните на код загрузчика. Вы увидите, что всех описанных выше проблем в нем попросту не возникает! (Этот же PHP-скрипт.) Листинг 9 скопировать код в буфер обмена php
// // ВНИМАНИЕ! До подключения библиотеки в браузер не должно быть выведено // ни одного символа. В противном случае функция header(), используемая // библиотекой, не сработает (см. документацию), и возникнет ошибка. // // Стартуем сессию. session_start(); // Подключаем библиотеку поддержки. require_once "../../../lib/config.php"; require_once "Subsys/JsHttpRequest/Php.php"; // Создаем главный объект библиотеки. // Указываем кодировку страницы (обязательно!). $JsHttpRequest =& new Subsys_JsHttpRequest_Php("windows-1251"); // Получаем запрос. $q = $_REQUEST['q']; // Формируем результат прямо в виде PHP-массива! $_RESULT = array( "q" => $q, "md5" => md5($q), 'hello' => isset($_SESSION['hello'])? $_SESSION['hello'] : null ); // Демонстрация отладочных сообщений. if (strpos($q, 'error') !== false) { callUndefinedFunction(); } echo "REQUEST_URI: ".$_SERVER['REQUEST_URI']." "; echo "Loader used: ".$JsHttpRequest->LOADER; ?> Итак, библиотека Subsys_JsHttpRequest_Php берет на себя всю «грязную работу» по «обертыванию» результата работы загрузчика в JavaScript-код. В программе достаточно лишь присвоить значение специальному массиву $_RESULT, и данные, благодаря слаженной работе frontend- и backend-частей библиотеки, благополучно поступят в браузер, сохранив свою структуру. К счастью, массивы и хэши JavaScript и PHP устроены практически одинаково, поэтому можно безболезненно производить перевод PHP-массива... Листинг 10 скопировать код в буфер обмена $_RESULT === array(
"q" => 'запрос', "md5" => 'MD5-код введенной строки' ) ...в JavaScript-хэш: Листинг 11 скопировать код в буфер обмена req.responseJS === {
q: 'запрос', md5: 'MD5-код введенной строки' } Ну а чтобы все окончательно прояснилось, приведу примерный результат работы скрипта load.php, как его видит браузер (уже после «обертывания» библиотекой Subsys_JsHttpRequest_Php): Листинг 12 скопировать код в буфер обмена Subsys_JsHttpRequest_Js.dataReady(
123, 'Отладочные сообщения.', { q: 'запрос', md5: 'MD5-код введенной строки' } ) Итак, простое присваивание значения массиву $_RESULT приводит к генерации вот такого вот JavaScript-кода. Он просто физически не может оказаться некорректным (ибо всегда создается по одинаковому шаблону, и кавычки с апострофами в нем экранируются backend-частью Subsys_JsHttpRequest_Php). Перехват ошибок в PHP-загрузчике «Обертывание» работает с использованием функции PHP ob_start(), которая позволяет перехватывать данные, поступающие в выходной поток скрипта, и производить с ними любые преобразования. По счастливой случайности, ob_start() позволяет также перехватывать ошибки, произошедшие в скрипте, в том числе фатальные, не поддающиеся перехвату никакими другими способами! Кстати, эта полезная особенность функции ob_start() довольно малоизвестна. Вы можете использовать ее и для других целей — например, чтобы гарантировано вывести нижнюю часть страницы даже в случае серьезного краха скрипта. Т.к. все сообщения об ошибках (например, вызов несуществующей функции) PHP печатает прямо в выходной поток (как будто бы через echo), логично воспринимать все содержимое выходного потока скрипта в качестве отладочного текста. Если вы помните, этот текст доступен в свойстве req.responseText, пустом при корректном завершении загрузчика. Благодаря механизму «обертывания» ни одна, даже самая серьезная, ошибка в PHP-программе не сгенерирует некорректного JavaScript-кода. Вместо этого текст ошибки попадет в третий параметр функции Subsys_JsHttpRequest_Js.dataReady(), и в итоге окажется в req.responseText. Вы можете убедиться, что перехват ошибок работает, введя на тестовой странице (см. выше) строчку, содержащую слово "error". Вы получите в нижнем динамическом поле сообщение: Листинг 13 скопировать код в буфер обмена Fatal error: Call to undefined function: callundefinedfunction()
in load.php on line 15 Решение проблемы с кодировкамиПри формировании запроса к загрузчику может потребоваться передать ему строки, содержащие русские буквы. Естественно, их нельзя напрямую передавать в URL, а вначале нужно URL-кодировать — преобразовать каждый символ русского алфавита к виду %XX, где XX — код символа. В JavaScript имеется функция escape(), которая URL-кодирует строку данных. К сожалению, она возвращает результат только в виде Unicode. Например, строка "проба" представляется ей так "%u043F%u0440%u043E%u0431%u0430". В PHP нет функций, умеющих раскодировать такое представление данных (urldecode() тут плохой помощник, ибо она не поддерживает формат %uXXXX). Функция escape() позволяет закодировать совершенно любой символ, будь то русская буква, литера греческого алфавита или даже китайский иероглиф. Лирическое отступление Вообще говоря, в последний версиях JavaScript имеется функция encodeURIComponent(), умеющая кодировать данные в обход Unicode. Однако она не поддерживается, например, в Internet Explorer 5.0, так что из соображения кроссбраузерности нам не подходит. К счастью, популярное расширение iconv для PHP поддерживает функцию для преобразования данных во всевозможных кодировках, так что перекодировать из Unicode в Windows-1251 не составляет для backend-библиотеки Subsys_JsHttpRequest_Php особых сложностей. Лирическое отступление По многочисленным просьбам, начиная с версии 3.0 библиотека Subsys_JsHttpRequest_Php может работать и без iconv, если основной кодировкой сайта является windows-1251 или koi8-r. Функции и таблицы перевода из Unicode в одну из этих однобайтовых кодировок встроены в сам модуль. Итак, вы можете вызывать метод send() объекта Subsys_JsHttpRequest_Js, не задумываясь о кодировках данных. Вам не нужно ничего перекодировать вручную ни в серверном, ни в клиентском коде: библиотека берет всю эту работу на себя. Еще раз: если вы хотите использовать библиотеку Subsys_JsHttpRequest с кодировками, отличными от windows-1251 и koi8-r (например, с UTF-8), на сервере должно быть установлено расширение PHP iconv. У большинства хостеров оно стоит, но, если вдруг окажется, что его нет (к позору провайдера), хостеру не составит труда установить модуль. Что нового в версии 3.0 Ранние версии библиотеки (1.x и 2.x) назывались JSHttpRequest. Они обладали несколько меньшей функциональностью, чем Subsys_JsHttpRequest. Новая версия — 3.x — поддерживает следующие возможности, недоступные ее предшественнице: * Если браузер имеет встроенный объект XMLHttpRequest или же ActiveX-элемент Msxml2.XMLHTTP/Microsoft.XMLHTTP, и при этом не возникает проблем с безопасностью (обращение к загрузчику на том же самом домене), они автоматически задействуются. В противном случае — используется метод динамического создания элемента <SCRIPT>. * В режиме использования XMLHttpRequest (или соответствующего ActiveX) поддерживается метод отправки запроса POST (указывается при вызове open()). Однако, если с POST возникают проблемы (например, в Opera 8.01), библиотека автоматически переключается на стандартный метод GET. Это же происходит, когда XMLHttpRequest не поддерживается браузером — ошибок в любом случае не выдается. * URL загрузчика, передаваемый методу open(), теперь может содержать параметры (например, load.php?test=abc). * Параметры теперь передаются в QUERY_STRING совершенно обычным способом, традиционным для PHP- и CGI-скриптов. Типичный URL запроса к загрузчику: load.php?test=abc&q=%u043F%u0440%u043E%u0431%u0430&4. Здесь: 1. load.php?test=abc — адрес загрузчика, указанный в методе open(). 2. &4 — «безвредная» добавка, содежащая ID сессии загрузки (уникальна для каждого запроса). 3. q=%u043F%u0440%u043E%u0431%u0430 — параметры, переданные в методе send(). * Полностью поддерживаются стандартные сессии PHP, причем backend библиотеки Subsys_JsHttpRequest_Php о них «ничего не знает», передавая полномочия управления сессиями интерпретатору PHP. Параметр PHPSESSID, указанный либо в куках, либо в GET, передается frontend-ом Subsys_JsHttpRequest_Js PHP-загрузчику (в дополнение к обычным GET-параметрам). Соответственно, если скрипт-frontend установил какие-то переменные в сессию, загрузчик-backend может их прочитать стандартными средствами. * Параметры загрузчику (в методе send()) можно передавать в стандартной PHP-форме (например, с именами a[abc], b[xyz] и т. д.). Т.е. разбор QUERY_STRING — на плечах стандартных функций PHP, а не идет вручную, как в предыдущей версии. * В случае, если базовая кодировка сайта — windows-1251 или koi8-r, а расширение iconv в PHP не подключено, используется своя собственная функция перекодирования. Так что в подавляющем большинстве случаев библиотека будет корректно работать с русскими буквами даже при отсутствии iconv! Еще большая кроссбраузерность? У меня есть большое подозрение, что трюк с динамической генерацией тэга <SCRIPT> сработал бы и в других, более старых версиях браузеров (по крайней мере, в IE4). Однако в этом случае код библиотеки уже не был бы таким универсальным — пришлось бы «привязываться» к особенностям браузеров. Кроме того, старые версии не поддерживают DOM на должном уровне (имеются в виду функции getElementById(), createElement(), appendChild() и т. д.), в то время как класс Subsys_JsHttpRequest_Js использует исключительно DOM. В IE, например, можно было бы использовать document.all и присавивания значений свойству innerHTML некоторого элемента. Если вы решите модифицировать библиотеку так, чтобы она работала и в старых браузерах, я буду рад получить от вас измененный код, а также обсудить его нюансы в форуме. Резюме Подведем итоги этой большой наблы. Вначале я перечислю ссылки на программные модули, упоминаемые выше. * JavaScript-frontend библиотеки Subsys_JsHttpRequest_Js: Subsys/JsHttpRequest/Js.js. * PHP-backend библиотеки Subsys_JsHttpRequest_Php: Subsys/JsHttpRequest/Php.php. Пример использования библиотеки: * Все примеры из статьи (zip). * Клиентский код (JavaScript): test.htm. * Серверный код загрузчика (PHP): load.php. Библиотека Subsys_JsHttpRequest активно используется на форуме forum.dklab.ru. А именно, через нее реализованы следующие функции: * "Живой поиск": на каждой странице форума имеется поле, в которое можно ввести поисковый запрос и сразу же получить результат, минуя перезагрузку страницы. * "Живой поиск" в форме добавления нового топика: то же самое, но срабатывает при вводе темы нового сообщения. * "Живой предпросмотр": рядом со ссылками на топики приведена специальная пиктограмма, наведя мышь на которую, можно просмотреть первое сообщение топика. * "Живая карма": пользователи могут изменять карму (рейтинг) друг другу, не перезагружая страницу. Источник: http://www.srubil.ru | |
| |
Просмотров: 378 | | |
Всего комментариев: 0 | |