Логин:   Пароль:






Новости
Рассылки
Форум
Поиск


Java
- Апплеты
- Вопрос-ответ
- Классы
- Примеры
- Руководства
- Статьи
- IDE
- Словарь терминов
- Скачать

Мобильная Java
- Игры
- Примеры
- Статьи
- WAP, WML и пр.

JavaScript
- Вопрос-ответ
- Примеры
- Статьи

Веб-мастеринг
- HTML
- CSS
- SSI

Разминка для ума
Проекты
Книги
Ссылки
Программы
Юмор :)




Rambler's Top100
Rambler's Top100

JavaScript: СтатьиJSHttpRequest: динамическая подкачка данных без перезагрузки страницы

JSHttpRequest: динамическая подкачка данных без перезагрузки страницы

В данной статье описывается подход к Remote Scripting, реализацию которого я до сих пор не видел ни на одном сайте. Более того, мне не известен ни один человек, который бы о нем знал заранее. Есть шанс, что метод уникален, поэтому, если (в особенности если!) это не так, пожалуйста, сообщите в форум, где и когда вы видели его реализацию.

В последние несколько месяцев определенную популярность получил новый сервис Google: так называемый Google Suggest. Те, кто еще не видел, что это такое, могут посмотреть прямо сейчас: http://www.google.com/webhp?complete=1&hl=en.

Пример автодополнения запроса в Google Suggest

Работа Google Suggest заключается в том, что по нескольким введенным буквам специальная программа на JavaScript обращается к сайту Google и запрашивает у него 10 самых «популярных» слов, начинающихся с тех же букв. Скрипт срабатывает настолько быстро, что выпадающий список с вариантами появляется практически мгновенно. Естественно, перезагрузка страницы при этом не производится — все реализовано на JavaScript и DHTML.

Для реализации «динамической подгрузки» Google использует следующие средства:

  1. В Internet Explorer: ActiveX-компонента с именем Msxml2.XMLHTTP или Microsoft.XMLHTTP.
  2. В Mozilla и FireFox: встроенный класс XMLHttpRequest.
  3. В Opera: динамически создаваемый <IFRAME> нулевого размера (скрытый).

Про то, как работает Google Suggest, в Интернете пишут все, кому не лень, и я совершенно не собираюсь повторяться. Вместо этого я представлю новый подход под названием JSHttpRequest, обходящий Google Suggest по совместимости с различными браузерами.

Начнем с примера

Чтобы вам не было чересчур скучно, я сразу демонстрирую библиотеку JSHttpRequest «в действии». Введите несколько слов в текстовом поле внизу — через 2 секунды появятся результаты поиска по этим словам на форуме http://forum.dklab.ru/. Страница при этом перезагружена не будет.

Забегая вперед, скажу, что JSHttpRequest не опирается на особенности браузеров и работает в IE5.0+, Mozilla 1.7+, FireFox 1.0+ и Opera 7.3+ (здесь «+» означает «в этой и более новых версиях»). Кроме того, код не использует ни ActiveX, ни <IFRAME>, а построен на базе динамической генерации тэга <SCRIPT>.

Недостатки подхода Google Suggest

Итак, в разных браузерах Google применяет совершенно различные методы подгрузки. Рассмотрим их недостатки.

  1. Т.к. в IE используется ActiveX-компонента, вы должны включить ActiveX в настройках браузера. И хотя по умолчанию данная функция как раз включена, многие пользователи, наслышанные о многочисленных дырах IE, вручную ее отключают.

    Несмотря на это, вы все же можете убедиться, что Google Suggest продолжает работать и после выключения ActiveX. Видимо, задействуется механизм, основанный на <IFRAME> (как в Opera). В любом случае, на ваших сайтах Microsoft.XMLHTTP при выключенных ActiveX работать не будет (это проверено). Про недостатки <IFRAME>-метода сказано ниже.

  2. Класс XMLHttpRequest, используемый в Mozilla и FireFox, в настоящий момент присутствует только в этих браузерах, и больше нигде. У него есть небольшой недостаток: при умолчательных настройках FireFox запрещено загружать данные откуда-то, кроме как с текущего сайта.
  3. Применение динамически создаваемого <IFRAME> связано с массой проблем. Главный недостаток — при изменении атрибута src у <IFRAME> раздается характерный щелчок и добавляется запись в «историю браузера», так что кнопка Back (Назад) начинает работать неправильно. И хотя данный «подводный камень» можно обойти (весьма искусственным способом), возникают новые проблемы, различные в разных браузерах. Я не буду их сейчас перечислять; скажу только, что за 2 дня перепробовал множество (штук 20) всевозможных вариантов, но добиться кроссбраузерного кода, работающего одинаково и без посторонних эффектов во всех браузерах, мне так и не удалось. Другой недостаток <IFRAME> — большой расход памяти и медлительность: фактически, для каждого фрейма создается новый отдельный браузер, который независимо обрабатывает загруженный HTML-код.

Короче говоря, Google использует разные (ортогональные, несовместимые) подходы в различных браузерах.

Метод, который реализует динамическую подгрузку в Google Suggest, проиллюстрирован ниже на примере загрузки исходного текста текущей страницы. (Работу с <IFRAME> я здесь не привожу, потому что она довольно сложна. Речь идет только о классе XMLHttpRequest и ActiveX-компоненте Microsoft.XMLHTTP).

Листинг 1
<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, а также пользователи, выключившие себе ActiveX по соображениям безопасности, «отдыхают».

Принцип работы библиотеки JSHttpRequest

Многочисленных проблем и особенностей с ActiveX, XMLHttpRequest и <IFRAME> можно избежать, если... не использовать данные технологии. Это звучит действительно банально, однако — работает!

Дело в том, что существует один прекрасный и крайне кроссбраузерный способ загрузки данных на страницу. Очень странно, что разработчики Google до него не догадались. Речь о динамическом создании и присоединении к текущей странице тэга <SCRIPT>. Такому тэгу следует указать атрибут src, совпадающий с адресом серверного скрипта подгрузки данных (написанного, к примеру, на PHP).

Конечно, загружаемый скрипт должен выдавать корректный код на JavaScript. Обычный текст таким методом не подгрузишь.

Рассмотрим на примере, как работает данный подход. Предположим, что при нажатии на кнопку JavaScript-программа вставляет (c использованием DHTML) в текущую страницу следующий тэг:

Листинг 2

<script language="JavaScript" 
 src="load.php?123:arguments"></script>

Что при этом произойдет? Браузер немедленно обратится к серверу со следующим запросом:

Листинг 3

load.php?123:arguments

В результате на сервере запустится скрипт load.php, который получит в QUERY_STRING параметры 123:arguments (конечно, аргументы могут быть произвольными). Программа отработает (к примеру, обратится к базе данных) и напечатает в качестве результирующей страницы следующий текст:

Листинг 4

JSHttpRequest.dataReady(
  123,
  [
    'Это некоторые данные.',
    'Они могут иметь произвольную структуру...',
    { test: '...и вложенность' }
  ],
  'А здесь идет простой отладочный текст.'
)

Если вы не поняли, к чему все эти квадратные и фигурные скобки, скорее прочитайте тридцать восьмую наблу, в которой рассматривуются особенности синтаксиса JavaScript.

Итак, PHP-скрипт load.php напечатал в свой выходной поток текст, являющийся по совместительству корректной JavaScript-программой. Он будет использован браузером в качестве источника данных произвольной структуры.

М-ммм... «Программа, пишущая другие программы»... «Источник»... Определенно «Matrix has you».

В итоге код на JavaScript, сгенерированный PHP-скриптом load.php, будет выполнен браузером! Как видите, вызывается функция dataReady() библиотеки JSHttpRequest, которой передается:

  1. Уникальный идентификатор загрузки (чтобы не спутать одни данные с другими, ведь страница может одновременно запросить сведения сразу из нескольких источников).
  2. Произвольные данные, полученные программой load.php, например, из БД.
  3. Некоторый текст, который может быть использован в отладочных целях (например, там удобно указывать сообщения об ошибках, возникших в PHP-программе).

Ну а уж функция JSHttpRequest.dataReady() заботится о доставке загруженых данных конечному потребителю, осуществляя также кэширование одинаковых запросов (если это разрешено).

Динамическая генерация тэга <SCRIPT> имеет одно важное достоинство: при использовании такого подхода «история» браузера (history) не засоряется лишними ссылками, а при загрузке не слышно щелчка, издаваемого многими браузерами во время перехода на другую страницу. Нужно также заметить, что в FireFox имеется небольшая ошибка, в результате которой статус-строка не очищается после загрузки <SCRIPT>-компонента (в ней остается сообщение "Loading ..."). Впрочем, эта ошибка ни на что не влияет и, вероятно, будет в скором времени исправлена разработчиками.

Библиотека JSHttpRequest состоит из двух частей, работающих совместно друг с другом:

  • JSHttpRequest.js, 3 КБ: JavaScript-код, определяющий класс JSHttpRequest. Это — так называемый frontend системы («передний проход»). Его следует подключать к страницам с помощью тэга:

    Листинг 5

    <script language="JavaScript" src="JSHttpRequest.js">
    </script>
  • JSHttpRequest.php, 3.5 КБ: PHP-код, в котором определяются функции для облегчения написания загрузчиков на PHP. Это — так называемый backend системы («задний проход»). Его следует включать в самое начало программы оператором:

    Листинг 6

    require_once "JSHttpRequest.php";

В качестве языка для написания загрузчиков выбран PHP, потому что он:

  • Весьма распространен.
  • Крайне быстр, если приходится работать с маленькими скриптами, коими как раз и являются загрузчики. (Это, естественно, касается только mod_php — он так чаще всего и ставится хостерами.)
  • В большинстве случаев имеет встроенную поддержку Unicode (расширение iconv), которая, как вы увидите ниже, нам очень понадобится.

Можно, конечно, писать скрипты загрузки и на CGI-perl, однако в этом случае нагрузка на сервер резко возрастет, что для динамической подгрузки данных противопоказано. Ну а mod_perl встретишь далеко не на каждом хостинге.

JSHttpRequest: JavaScript-frontend

Использовать класс JSHttpRequest в JavaScript-программе совсем просто. Собственно, его интерфейс практически не отличается от интерфейсов FireFox-овского XMLHttpRequest или IE-шного Microsoft.XMLHTTP (он специально так разрабатывался).

Приведу пример страницы, которая обеспечивает генерацию хэш-кода MD5 для введенной пользователем строки. Само вычисление происходит на сервере, а браузер лишь обращается к последнему за данными, используя библиотеку JSHttpRequest. (Этот же пример в действии.)

Листинг 7

<script language="JavaScript" 
 src="../JSHttpRequest.js"></script>
<script>
  // Вызывается по тайм-ауту или при щелчке на кнопке.
  function doLoad(force) {
    // Получаем текст запроса из <input>-поля.
    var query = '' + document.getElementById('query').value;
    // Создаем новый объект JSHttpRequest.
    var req = new JSHttpRequest();
    // Код, АВТОМАТИЧЕСКИ вызываемый при окончании загрузки.
    req.onreadystatechange = function() {
if (req.readyState == 4) {
  // Записываем в <div> результат работы. 
  document.getElementById('result').innerHTML = 
    'MD5("'+req.responseJS.q+'") = ' +
    '"' + req.responseJS.md5 + '"';
  // Отладочная информация.
  document.getElementById('debug').innerHTML = 
    req.responseText;
}
    }
    // Разрешаем кэширование (чтобы при одинаковых запросах
    // не обращаться к серверу несколько раз).
    req.caching = true;
    // Подготваливаем объект.
    req.open('GET', 'load.php', true);
    // Посылаем данные запроса (задаются в виде хэша).
    req.send({ q: query });
  }
  // Поддержка загрузки данных по тайм-ауту (1 секунда после
  // последнего отпускания клавиши в текстовом поле).
  var timeout = null;
  function doLoadUp() {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(doLoad, 1000);
  }
</script>
<!-- Форма -->
<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>

Из-за обилия комментариев выглядит страшно, однако, если внимательно посмотреть, хорошо видно, что применение JSHttpRequest ничем принципиальным не отличается от использования XMLHttpRequest или Microsoft.XMLHTTP.

Имеется одна важная особенность библиотеки: результат работы load.php удобно получать из свойства req.responseJS. Как видно, в него загрузчик помещает следующий хэш:

Листинг 8
{ 
  q:   'запрос', 
  md5: 'MD5-код введенной строки' 
}

В поле req.responseText хранятся данные, выданные скриптом load.php в свой выходной поток (операторами echo). В большинстве случаев они содержат лишь сообщения об ошибках (если ошибки имели место), и именно поэтому данное свойство трактуется как отладочное.

Впрочем, ничто не мешает написать загрузчик так, чтобы он передавал основной результат своей работы именно в виде req.responseText (хотя это и не очень удобно — см. ниже).

JSHttpRequest: PHP-backend

Теперь пришло время посмотреть, как выглядит загрузчик load.php. Помните, мы говорили, что результатом его работы должен быть текст, являющийся корректным JavaScript-кодом. Т.е. просто вывести "Привет, это сгенерированные данные!" нельзя — нужно вначале «обернуть» их вызовом функции JSHttpRequest.dataReady(). Вы можете подумать: сколько же мороки возникает из-за этого... Ведь достаточно допустить одну маленькую ошибку (к примеру, пропустить запятую), как результирующий код перестанет быть корректным, в то время как загрузчик не выдаст никакой ошибки.

Но взгляните на код загрузчика. Вы увидите, что всех описанных выше проблем в нем попросту не возникает! (Этот же PHP-скрипт.)

Листинг 9

<?php
// Подключаем библиотеку поддержки.
require_once "../JSHttpRequest.php";
// Создаем главный объект библиотеки.
// Указываем кодировку страницы (обязательно!).
$JSHttpRequest = new JSHttpRequest("windows-1251");
// Получаем запрос.
$q = $_REQUEST['q'];
// Формируем результат прямо в виде PHP-массива!
$_RESULT = array(
  "q"   => $q,
  "md5" => md5($q)
); 
// Демонстрация отладочных сообщений.
if (strpos($q, 'error') !== false) {
  callUndefinedFunction();
}
?>

Итак, библиотека JSHttpRequest.php берет на себя всю «грязную работу» по «обертыванию» результата работы загрузчика в JavaScript-код. В программе достаточно лишь присвоить значение специальному массиву $_RESULT, и данные, благодаря слаженной работе frontend- и backend-частей библиотеки, благополучно поступят в браузер, сохранив свою структуру.

К счастью, массивы и хэши JavaScript и PHP устроены практически одинаково, поэтому можно безболезненно производить перевод PHP-массива...

Листинг 10

$_RESULT === array(
  "q"   => 'запрос',
  "md5" => 'MD5-код введенной строки'
)
...в JavaScript-хэш:

Листинг 11

req.responseJS === { 
  q:   'запрос', 
  md5: 'MD5-код введенной строки' 
}

Ну а чтобы все окончательно прояснилось, приведу примерный результат работы скрипта load.php, как его видит браузер (уже после «обертывания» библиотекой JSHttpRequest.php):

Листинг 12

JSHttpRequest.dataReady(
  123,
  'Отладочные сообщения.',
  { 
    q:   'запрос', 
    md5: 'MD5-код введенной строки' 
  }
)

Итак, простое присваивание значения массиву $_RESULT приводит к генерации вот такого вот JavaScript-кода. Он просто физически не может оказаться некорректным (ибо всегда создается по одинаковому шаблону, и кавычки с апострофами в нем экранируются backend-частью JSHttpRequest.php).

Перехват ошибок в PHP-загрузчике

«Обертывание» работает с использованием функции PHP ob_start(), которая позволяет перехватывать данные, поступающие в выходной поток скрипта, и производить с ними любые преобразования. По счастливой случайности, ob_start() позволяет также перехватывать ошибки, произошедшие в скрипте, в том числе фатальные, не поддающиеся перехвату никакими другими способами!

Кстати, эта полезная особенность функции ob_start() довольно малоизвестна. Вы можете использовать ее и для других целей — например, чтобы гарантировано вывести нижнюю часть страницы даже в случае серьезного краха скрипта.

Т.к. все сообщения об ошибках (например, вызов несуществующей функции) PHP печатает прямо в выходной поток (как будто бы через echo), логично воспринимать все содержимое выходного потока скрипта в качестве отладочного текста. Если вы помните, этот текст доступен в свойстве req.responseText, пустом при корректном завершении загрузчика. Благодаря механизму «обертывания» ни одна, даже самая серьезная, ошибка в PHP-программе не сгенерирует некорректного JavaScript-кода. Вместо этого текст ошибки попадет в третий параметр функции JSHttpRequest.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-библиотеки JSHttpRequest.php особых сложностей.

Итак, вы можете вызывать метод send() класса JSHttpRequest, не задумываясь о кодировках данных. Вам не нужно ничего перекодировать вручную ни в серверном, ни в клиентском коде: библиотека берет всю эту работу на себя.

Конечно, не забудьте, что если вы хотите использовать библиотеку JSHttpRequest, на сервере должно быть установлено расширение PHP iconv. У большинства хостеров оно стоит, но, если вдруг окажется, что его нет (к позору провайдера), хостеру не составит труда установить модуль.

Еще большая кроссбраузерность?

У меня есть большое подозрение, что трюк с динамической генерацией тэга <SCRIPT> сработал бы и в других, более старых версиях браузеров (по крайней мере, в IE4). Однако в этом случае код библиотеки уже не был бы таким универсальным — пришлось бы «привязываться» к особенностям браузеров. Кроме того, старые версии не поддерживают DOM на должном уровне (имеются в виду функции getElementById(), createElement(), appendChild() и т. д.), в то время как класс JSHttpRequest использует исключительно DOM. В IE, например, можно было бы использовать document.all и присавивания значений свойству innerHTML некоторого элемента.

Если вы решите модифицировать библиотеку так, чтобы она работала и в старых браузерах, я буду рад получить от вас измененный код, а также обсудить его нюансы в форуме.

Резюме

Подведем итоги этой большой наблы. Вначале я перечислю ссылки на программные модули, упоминаемые выше.

Пример использования библиотеки:

  • Клиентский код (JavaScript): test.htm.
  • Серверный код загрузчика (PHP): load.php.

Библиотека JSHttpRequest активно используется на форуме forum.dklab.ru. А именно, через нее реализованы следующие функции:

  • "Живой поиск": на каждой странице форума имеется поле, в которое можно ввести поисковый запрос и сразу же получить результат, минуя перезагрузку страницы.
  • "Живой поиск" в форме добавления нового топика: то же самое, но срабатывает при вводе темы нового сообщения.
  • "Живой предпросмотр": рядом со ссылками на топики приведена специальная пиктограмма, наведя мышь на которую, можно просмотреть первое сообщение топика.
  • "Живая карма": пользователи могут изменять карму (рейтинг) друг другу, не перезагружая страницу.

Статьи в Интернете на тему



Дмитрий Котеров
Лаборатория dk
[3 февраля 2005г.]

Warning: mysql_connect() [function.mysql-connect]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/books/show2b.php on line 11

Warning: mysql_db_query() [function.mysql-db-query]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/books/show2b.php on line 19

Warning: mysql_db_query() [function.mysql-db-query]: A link to the server could not be established in /pub/home/javaport/javaportal/books/show2b.php on line 19

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /pub/home/javaport/javaportal/books/show2b.php on line 30
Узнай о чем ты на самом деле сейчас думаешь тут.


[an error occurred while processing this directive]



Warning: mysql_connect() [function.mysql-connect]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/news/worldnews.php on line 91

Warning: mysql_db_query() [function.mysql-db-query]: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2) in /pub/home/javaport/javaportal/news/worldnews.php on line 93

Warning: mysql_db_query() [function.mysql-db-query]: A link to the server could not be established in /pub/home/javaport/javaportal/news/worldnews.php on line 93

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in /pub/home/javaport/javaportal/news/worldnews.php on line 95