Как создавать простые AJAX запросы к серверному скрипту (PHP) вы уже научились, методами простого XML, а так же с использованием библиотеки prototype. Рассмотрим более комплексный пример - как организовать поиск по базе данных MySQL средствами AJAX и PHP.

В первой части нашего урока, мы рассмотрим как всё это сделать типичным выводом XML данных через PHP скрипт, а во второй части я попытаюсь привести аналогичный пример с использованием библиотеки prototype, а так же подвести небольшую сравнительную характеристику.

Сразу прошу обратить внимание, что статья написано о том, КАК организовать AJAX поиск, а не о том, как правильно остерегаться SQL инъекции, как лучше выводить поисковую информацию и о том, что такое индексация таблиц для полнотекстового поиска. Мы здесь изучим основы составления запроса, а методы поиска, защита, красочный и организованный вывод остаются за вами.

Наглядный пример

Чтобы вы были в курсе, что мы тут попытаемся написать, прошу взглянуть: http://logicerror.pp.ru/upload/ajax_search_xml/
Вы вводите ключевую фразу в текстовое поле, жмёте на кнопку поиска, и вуала, результаты поиска без перезагрузки страницы!

Как это работает? Всё очень просто. При нажатии на кнопку поиска, форма никуда не ведёт, а просто вызывает javascript функцию, которая, прочитав ваш поисковой запрос, отсылает его серверному скрипту (php). Этот скрипт, предварительно обработав ваш запрос, ищет совпадения в базе данных, и выводит результаты соответственно в XML формате. Получив данные, javascript отображает их в соответствующем формате.

То есть у нас имеется:

  1. база данных MySQL, где хранятся все данные
  2. файл HTML (index.html) - форма, и поле для вывода результатов
  3. файл JS (script.js) - ajax-driven скрипт - исполнитель запросов (пускай будет первого уровня), организатор вывода результатов
  4. файл PHP (search.php) - исполнитель запросов (второго уровня), и вывод результатов в виде XML для дальнейшей обработки

Рассмотрим по порядку.

Источник данных

Здесь говорить особо не о чем. В общем, пускай у нас будет небольшой сборник статей. С базами данных все уже работали, так что сразу к структуре:

CREATE TABLE `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) collate utf8_unicode_ci NOT NULL,
  `content` text collate utf8_unicode_ci NOT NULL,
  `author` varchar(255) collate utf8_unicode_ci NOT NULL,
  `timestamp` int(11) NOT NULL,
  UNIQUE KEY `id` (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1;

Работать мы будем всего с двумя полями, остальные берем как запас – вдруг пригодятся. Поле title – заголовок статьи, и поле content – собственно статья.

Далее, наполняете свою таблицу мусором (если кому в голову мусор не приходит, можно взять мусор здесь: http://logicerror.pp.ru/upload/ajax_search_xml/junk.sql).

HTML visual – страница вывода

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Language" content="ru">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>AJAX PHP search example - XML</title>
 
<link rel="stylesheet" href="style.css" type="text/css">
<script src="script.js" type="text/javascript"></script>
 
</head>
<body>
<div id="wrap">
<form onsubmit="search(); return false;">
<input type="text" class="input" id="search_input" value=""> <input type="submit" class="button" id="search_button" value="&raquo; поиск">
</form><br>
 
<div id="search_results">
</div>
 
<div id="searching">
	searching
</div>
 
</div>
</body>
</html>

Проблем здесь возникнуть тоже не должно, поскольку вы прекрасно (надеюсь) знаете, что такое id и чем отличается от class.

Объясню некоторые важные моменты:

  • search_input – текстовое поле для ввода строки поиска
  • search_results – поле для вывода результатов
  • searching – надпись «подождите, идет поиск…»

Ну а как вы, наверное, уже догадались, при отправки формы (onsubmit) мы вызываем функцию search(); и возвращаем false (думаю, никто уже не посмеет спросить зачем).

CSS в комплекте: http://logicerror.pp.ru/upload/ajax_search_xml/style.css

AJAX – посредник информации

Давайте сперва взглянем на скрипт целиком, а затем рассмотрим по частям.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function getXmlHttp() {
  var xmlhttp;
  try {
    xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) {
      try {
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (E) {
      xmlhttp = false;
    }
  }
 
  if (!xmlhttp && typeof XMLHttpRequest != 'undefined') {
    xmlhttp = new XMLHttpRequest();
  }
  return xmlhttp;
}
 
function search() {
  var sSearch = document.getElementById("search_input").value;
 
  if (sSearch.length < 3)
  {
    alert("Запрос должен быть не короче 3-х символов.");
    return false;
  }
 
  var xmlHttp;
  xmlHttp = getXmlHttp();
 
  var obj = document.getElementById("search_results");
  obj.innerHTML = "";
  var loading = document.getElementById("searching");
  loading.style.display = "block";
 
  xmlHttp.onreadystatechange = function()  {
    if (xmlHttp.readyState == 4)
    {
      loading.style.display = "none";
      var xmlDoc = xmlHttp.responseXML.documentElement.getElementsByTagName("entry");
      for (i = xmlDoc.length-1; i>= 0 ; i--)
      {
	    var new_el = document.createElement("div");
	    var sTitle = xmlDoc[i].getElementsByTagName("title")[0].firstChild.nodeValue;
	    var sContent = xmlDoc[i].getElementsByTagName("content")[0].firstChild.nodeValue;
 
	    new_el.innerHTML = "<h1>"+sTitle+"</h1>"+sContent;
	    new_el.className = "result";
 
	    obj.appendChild(new_el);
	  }
    }
  }
 
  xmlHttp.open('GET', 'search.php?search='+sSearch, true);
  xmlHttp.send(null);
}

Функцию getXmlHttp() мы уже с вами разбирали, а вот на функции search() остановимся по подробнее.

Сразу забегу чуток вперед, так как нужно знать в каком формате нам приходят XML данные, перед тем как их обрабатывать. Кусок результатов запроса:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<searchresults>
<entry>
	<title>Мобильный путеводитель по итогам прошедшей недели</title>
	<content>Мobime снова тепло встречает вас на на своей еженедельной новостной страничке. Просьба не толпиться, а тихонько занять места у своих мониторов – событий прошедшей недели хватит всем. Сегодня вы узнаете новые подробности о готовящемся коммуникаторе XP...</content>
</entry>
<entry>
	<title>Обзор Sony Ericsson K850i – часть первая</title>
	<content>Обычно во вступлении к таким знаковым продуктам упоминается предыстория создания, взгляды поклонников компании на примерный функциональный набор продукта, специфику анонсирования и собственно сам выход на прилавки магазинов. Ничего этого мы сегодня р...</content>
</entry>
</searchresults>

Корневое поле здесь у нас searchresults. От него следует ветка entry – запись. В записи мы имеем title – заголовок статьи, и content – небольшая часть самой статьи. Вернемся к нашему AJAX скрипту и пройдемся по порядку.

var sSearch = document.getElementById("search_input").value;
 
if (sSearch.length < 3)
{
  alert("Запрос должен быть не короче 3-х символов.");
  return false;
}

Здесь мы в переменную sSearch помещаем запрос, введенный в текстовое поле с идентификатором search_input, а так же проверяем его длину на значение больше 3-х (глупо искать по 3-м символам…).

var xmlHttp;
xmlHttp = getXmlHttp();

Создали объект xmlHttp с помощью функции getXmlHttp().

var obj = document.getElementById("search_results");
obj.innerHTML = "";
var loading = document.getElementById("searching");
loading.style.display = "block";

Очищаем предыдущие результаты поиска, при их наличии (блок search_results), а так же выводим строку «ждите, ищу» (помните?). Объект obj нам пригодится в дальнейшем для размещения новых результатов поиска.

xmlHttp.onreadystatechange = function()  {
  if (xmlHttp.readyState == 4)
  {

Здесь, уже известный нам, обработчик событий («слушатель»), и при состоянии объекта xmlHttp равное 4 (готов) начинаем обработку данных.

loading.style.display = "none";
var xmlDoc = xmlHttp.responseXML.documentElement.getElementsByTagName("entry");

Для начала можно спрятать надпись «ждите». Затем, создаем новый объект xmlDoc и записываем в него весь массив XML данных имеющих отношение к ветке entry (т.е. те данные, которые лежат внутри этой ветки (title, content)).

for (i = xmlDoc.length-1; i>= 0 ; i--)
{
  var new_el = document.createElement("div");

Здесь мы в цикле прогоняем каждый элемент массива и создаем для него новый элемент div. Но, перед тем, как прикрепить (приклеить) новый элемент в поле результатов, нужно его наполнить данными:

var sTitle = xmlDoc[i].getElementsByTagName("title")[0].firstChild.nodeValue;
var sContent = xmlDoc[i].getElementsByTagName("content")[0].firstChild.nodeValue;
 
new_el.innerHTML = "<h1>"+sTitle+"</h1>"+sContent;
new_el.className = "result";

В переменные sTitle и sContent записываем соответствующие данных из XML массива (title, content). Этот метод мы уже рассматривали в предыдущих уроках (ссылки на предыдущие уроки). Следом, записываем эти значения в наш новый элемент (для заголовка статьи используется тэг h1).

obj.appendChild(new_el);

Ну и здесь мы, наконец, приклеиваем наш новый элемент в поле результатов поиска (см. выше obj search_results). Обработчик событий готов, закрываем все операторные скобки и нам остается написать сам запрос.

xmlHttp.open('GET', 'search.php?search='+sSearch+'&rand='+Math.random(), true);
xmlHttp.send(null);

Простой AJAX GET запрос серверному скрипту search.php с передачей параметров sSearch - строка поиска, и случайно число во избежание кэширование на некоторых браузерах.

PHP – server-side coding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php
  $db = mysql_connect("localhost", "root", "");
  mysql_select_db("ajax_search");
  mysql_query("SET CHARACTER SET utf8");
 
  header('Content-type: application/xml; charset=utf-8');
  header('Cache-Control: no-cache');
 
  echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
  echo "<searchresults>\n";
 
  $sString = mysql_real_escape_string($_GET["search"], $db);
  $sql="SELECT * FROM `articles` WHERE `title` LIKE '%$sString%' OR `content` LIKE '%$sString%' ORDER BY `id` DESC LIMIT 10";
  $rs=mysql_query($sql,$db);
 
  if (mysql_num_rows($rs) > 0)
  {
    while ($row = mysql_fetch_array($rs))
    {
      $content=htmlspecialchars(strip_tags($row["content"]));
      if (mb_strlen($content, "utf-8") > 250) $content = mb_substr($content, 0, 250, "utf-8") . "...";
?>
<entry>
  <title><?=htmlspecialchars($row["title"]);?></title>
  <content><?=$content;?></content>
</entry>
<?
    }
  }
  else
  {
?>
<entry>
  <title> </title>
  <content>Ничего не найдено</content>
</entry>
<?
  }
?>
</searchresults>

Ну и наконец-то мы добрались до нашего главного дяди. Объяснять здесь тоже особо нечего. Прошу обратить внимание на одно. В предыдущих уроках мы использовали кодировку windows-1251 (cp1251, сравнение cp1251_general_ci), и мучались с переводом данных из utf8 в cp1251. Данный урок построен полностью на кодировке UTF-8, однако здесь возникла проблема с функциями strlen() и substr(). Для получения подстроки из кодировки UTF-8 следует использовать функцию mb_substr(), причем передав ей, 4-м параметром, строку «utf-8» (http://php.net/mb_substr). Функция mb_substr() работает аналогично (http://php.net/mb_strlen).

Разберем по порядку.

1
2
3
4
5
6
7
8
9
$db = mysql_connect("localhost", "root", "");
mysql_select_db("ajax_search");
mysql_query("SET CHARACTER SET utf8");
 
header('Content-type: application/xml; charset=utf-8');
header('Cache-Control: no-cache');
 
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n";
echo "<searchresults>\n";

Итак, подключаемся к серверу mysql, выбираем базу данных, указываем кодировку. Далее пару заголовков о том, что это за файл и как кэшировать. Затем стандартный XML заголовок и корневой элемент searchresults.

$sString = mysql_real_escape_string($_GET["search"], $db);
$sql="SELECT * FROM `articles` WHERE `title` LIKE '%$sString%' OR `content` LIKE '%$sString%' ORDER BY `id` DESC LIMIT 10";
$rs=mysql_query($sql,$db);

Строку поиска берем из HTTP GET заголовка (именно так нам передаёт ее ajax скрипт), причем обработав ее функцией mysql_real_escape_string(), и пишем запрос на поиск по базе. Кто не понял, как построен запрос в базу, советую подучить SQL.

$content=htmlspecialchars(strip_tags($row["content"]));
if (mb_strlen($content, "utf-8") > 250) $content = mb_substr($content, 0, 250, "utf-8") . "...";

Выбрав интересующие нас данные из базы, мы обрабатываем поле content, так как оно может быть достаточно длинным, а в результатах поиска не следует выводить статью целиком. Обрезаем ровно 250 символов и добавляем троеточие.

<entry>
  <title><?=htmlspecialchars($row["title"]);?></title>
  <content><?=$content;?></content>
</entry>

Ну а дальше выводим данные в соответствующем формате. Элемент entry (запись), внутри элементы title и content (заголовок и контент).

<entry>
  <title> </title>
  <content>Ничего не найдено</content>
</entry>

Здесь, в случае возврата 0 результатов, мы пишем точно такую же запись (т.е. в таком же формате), с пустым заголовком (используется пробел, так как javascript не очень любит пустые поля) и надписью «ничего не найдено».

Не забудьте закрыть корневой элемент searchresults.

И всё?

Нет, это далеко не всё, и пускать такое прямо в сеть еще рано. Не забывайте про безопасность в первую очередь. Ну и поработайте над внешним видом - например сделать это как мини-элемент на странице, небольшое окошко поиска. Не забудьте на результаты ссылочки добавить ;)

Полезные ссылки

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong