|
Не так давно определенную популярность получил новый сервис Google: так
называемый Google Suggest. Те, кто еще не видел, что это такое, могут посмотреть
прямо сейчас:
http://www.google.com/webhp?complete=1&hl=en.
Subsys_JsHttpRequest: подкачка данных без перезагрузки страницы (AJAX)
Работа 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>
<?show_source(__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 "<b>REQUEST_URI:</b> ".$_SERVER['REQUEST_URI']."<br>";
echo "<b>Loader used:</b> ".$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://dklab.ru/
|