AJAX. Так что же это?


Наша жизнь непостоянна. Все в этом мире эволюционирует и изменяется. В том числе и виртуальная реальность. И одно из слов, с которым связаны эти изменения,- это AJAX. Об AJAX уже слышали не только веб-программисты, но и рядовые пользователи. Что реально кроется за этой магической аббревиатурой? Как это использовать на своем сайте? На эти вопросы я и попытаюсь ответить в данной статье.

Впервые об AJAX я услышал весной этого года. Сразу заинтересовался этой технологией, и, как и положено в таких случаях, отправился в поиск за статьями, которые смогли бы ответить на возникшие у меня вопросы: "Что это такое? Как это работает? И в чем преимущества? Что нужно дополнительно установить на сервер/клиент для работы с AJAX? Как это можно использовать на своем сайте?". Прочтя с десяток статей, я получил ответ лишь на первые два вопроса, но на остальные ответа так нигде не нашел. И лишь после прочтения нескольких публикаций на английском языке я окончательно понял, что к чему. Это и подвигло меня к написанию данного материала.

В первую очередь статья адресуется подготовленным людям, пишущим программы для интернета и знакомым с такими терминами как "объект", "метод", "свойства". Однако частично может оказаться полезной и тем, кто просто интересуется данным вопросом. В списке литературы имеется необходимый перечень ссылок, воспользовавшись которыми вполне реально освоить технологию "с нуля".

По ходу изложения под термином "браузер" мы будем понимать браузеры: Internet Explorer 5.0+, Safari 1.3 и 2.0+, Netscape 7+, Opera 8.5+, Mozilla Firefox (плюс означает данную версию и более новые). Если речь станет заходить о других версиях, об этом будет упоминаться отдельно.

"Что это такое? Как это работает и в чем преимущества?"

Для того чтобы понимать, какие преимущества дает AJAX, нужно знать, как работают веб-приложения в настоящее время. А работают они по клиент-серверной технологии (рис. 1).

Работа веб-программы по клиент-серверной технологии
Работа веб-программы по клиент-серверной технологии

Пользователь в браузере открывает какую-либо страницу page. На странице есть гиперссылки, которые ведут на другие страницы. При нажатии на любую из них браузер посылает запрос URL на сервер, с которым связана эта ссылка. Если в природе не существует сервера, связанного с этой ссылкой (например, когда, набирая URL в адресной строке, вы ошиблись при написании имени ресурса), или имеются проблемы связи с интернетом, то браузер сгенерирует страницу, подобную показанной на картинке (так она выглядит в Operа-е):

В случае существования сервера, но отсутствии на нем документа, указанного в запросе сервер сам создаст HTML страницу с описанием ошибки. Например, это может быть всем известная 404-ая ошибка (документ не найден). Или, если все верно, в ответ сервер отдаст новую страницу. В любом случае, в браузер будет загружена новая страница new_page, даже если по сравнению со старой на ней изменилась лишь пара слов. Довольно существенный минус данной технологии. Кроме того, работа ведется в синхронном режиме. То есть после того как браузер отослал на сервер запрос он ожидает от него ответ, и пока ответ не получен ничего предпринимать не будет. А порой ответ, и загрузка новой страницы может длиться слишком долго. Настолько долго, что пользователь может не дождаться загрузки страницы и просто закрыть её. Поэтому веб-программмисты прибегают к некоторым уловкам.

Например, используя DOM методы языка JavaScript [1], можно динамически изменять фоновый рисунок блока без перезагрузки страницы. Это обеспечивает необходимую интерактивность. При наведении курсора мыши на ячейку таблицы исходная фоновая картинка pic_1.gif заменяется pic_2.gif. При уходе курсора с ячейки происходит обратный процесс.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<title>Смена фона</title>
<style type="text/css">
#td1 {
	background-image: url(image_1.gif);
}
</style>
</head>
<body>
<script language="javascript" type="text/javascript">
function change_background(id) {
	document.getElementById(id).style.backgroundImage = 'url(pic_2.gif)';
};
function source_background(id) {
	document.getElementById(id).style.backgroundImage = 'url(pic_1.gif)';
};
</script>
<table border="1" width="100%" height="50%">
	<tr>
		<td id="td1" onmouseover="change_background('td1')"
         onmouseout="source_background('td1')">&nbsp;</td>
		<td>&nbsp;</td>
	</tr>
	<tr>
		<td>&nbsp;</td>
		<td>&nbsp;</td>
	</tr>
</table>
</body>
</html>

Или такой пример. В файл с изображением в формате JPEG можно записать (и прочитать) метаданные EXIF. Многие интернет фоторесурсы позволяют для отображаемой фотографии показать и эти данные. Но они появляются только после нажатия соответствующей кнопки на экране. При этом обновление страницы не происходит. Делается это очень просто. EXIF данные находятся на странице, но расположены в скрытом блоке с помощью CSS (display: none). При нажатии на кнопку блок становится видимым (display: block). Минусы очевидны: вместе со страницей передаются данные, которые, возможно, не будут использованы; отобразить можно лишь те данные, которые были присланы вместе со страницей.

Когда-то клиент-серверная технология способствовала развитию сети. Но как это нередко бывает то, что двигало прогресс в определенный период истории, чуть позже начинает этот прогресс тормозить. В данном случае становилось очевидным, что клиент-серверная технология в чистом виде не может удовлетворить всем требованиям, которые предъявляются к некоторым веб-приложениям. Требовалось какое-то новое решение. И оно появилось в виде AJAX подхода к написанию веб-приложений.

Широкое распространение этого понятия началось с публикации в англоязычной части интернета статьи Джесси Джеймса Гарретта "Новый подход к веб-приложениям" в феврале 2005 года [2]. Статья писалась проектировщиком и для проектировщиков. Но со статьей ознакомилось много человек, в том числе и программисты. Которые и поспешили забросать автора электронными посланиями с вопросами о технических деталях реализации данного подхода. Писем, видимо, было настолько много, что уже в декабре того же года Гаррету в предисловии одной из книг по AJAX пришлось дать пояснения, что он не является программистом, и вопросы по конкретной реализации AJAX-приложений не к нему ("The truth is, I've never built an Ajax application. Sure, I've worked on Ajax projects. ...").

Так в чем ключевое отличие AJAX технологии от классической клиент-серверной? Это наличие AJAX-движка, состоящего из двух частей: клиент-приложение (написанное на языке JavaScript) и сервер-приложение (написанное на любом серверном языке). Также немного другой логикой общения приложения-клиента с сервером (рис. 2).

Работа веб программы по технологии AJAX
Работа веб-программы по технологии AJAX

Теперь при активации какого-либо элемента управления интерфейса браузер не делает запрос новой страницы с сервера, а запускает клиентскую часть. А уже приложение-клиент, в свою очередь, обращается к серверу через запрос requst и запрашивает только те данные, которые должны измениться на странице. После получения данных data от приложения-сервера клиент-приложение производит обновление части страницы через DOM методы без перегрузки всей страницы в целом. При этом возможна работа в асинхронном режиме. То есть когда пользователь не дожидается получения ответа с сервера и перегрузки страницы, а продолжается работать со страницей page, как ни в чем не бывало.

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

Тут мне хочется сделать небольшое лирическое отступление. Немало статей, в которых говорится о том, что AJAX это не технология, это подход к написанию приложения, идея. В принципе, верно, все, что нас окружает и есть идеи, но лично я настаиваю на термине "технология AJAX" для подчеркивания того факта, что это еще одна технология взаимодействия клиентского и серверного приложений (наряду с клиент- и файл-серверными технологиями).

"Что нужно дополнительно установить на сервер/клиент для работы с AJAX?"

Все вышеизложенное было чисто теоретическими рассуждениями разработчика, которые и написал Гарретт. Настало время перейти к практическому рассмотрению того, на чем основывается AJAX и что конкретно нужно использовать для написания AJAX-приложений.

AJAX расшифровывается как Asynchronous JavaScript + XML (асинхронный JavaScript+XML) и уже это указывает, на что опирается технология. А опирается практически на все то же самое, что и другие веб-приложения:

  • HTML/XHTML [3]/[4] для написания разметки страницы;
  • CSS [5] для визуального оформления страницы;
  • DOM [1] для динамического изменения страницы в ответ на действия пользователя;
  • XML [6, 7] для обмена данными между клиентской и серверными частями;
  • JavaScript [8] собственно для написания AJAX движка, для обеспечения интерактивности;
  • XMLHttpRequest объект [9] для осуществления запросов к серверу.

Когда я решил поработать с AJAX-приложением, то подумал, что для поддержки нужно что-то поставить на сервер. Какой-либо модуль, может, в виде dll. Но как потом понял, ничего никуда устанавливать не нужно. Требуется лишь корректно написать движок и все. Возникает вопрос, а что за AJAX-движки распространяются в сети? Ответ прост: это уже готовые приложения, которые предназначены для каких-либо задач, и которые в готовом виде можно использовать на своем сайте. Например, существует библиотека для PHP, которая называется xAjax. Она включает в себя как клиентский скрипт, написанный на JavaScript, так и серверный скрипт, с которым JavaScript взаимодействует. Помните об этом! У меня был случай, когда человек думал, что AJAX-приложение это только JavaScript, использующий XMLHttpRequest объект, и все. Но это далеко не все. Есть еще скрипт на стороне сервера, логику работы которого также придется проектировать, а потом писать код.

Рассматривать готовые библиотеки я не буду. В документации, идущей с ними, все расписано достаточно подробно. Остается лишь, используя их API, подключить их к своему сайту. Наша цель ознакомится с начинкой таких приложений. Эти знания помогут, как самому написать Ajax-приложение, так и разобраться в уже готовом решении.

Основа любого AJAX-приложения - это XMLHttpRequest объект, с его рассмотрения и начнем.

У данного объекта немало свойств и методов [10], но не все из них поддерживаются ведущими браузерам. Полная поддержка есть только в FireFox-е. Поэтому привожу только то, что будет работать в современных браузерах [9].

СВОЙСТВА:

readonly onreadystatechange function

Указывает функцию обратного вызова (callback function), которая будет вызываться каждый раз, когда будет изменяться readyState свойство. Несмотря на то, что вызывается функция, параметры передать в нее не получиться. Но об этом чуть позже в примере.

readonly readyState integer

Состояние запроса. Может принимать значения:
  • 0 - не неинициализированный (uninitialized), метод open() еще не был вызван;
  • 1 - загружается (loading), метод send() еще не вызван;
  • 2 - загружен (loaded), метод send() был вызван и ответные заголовки/статус (свойство status) получены;
  • 3 - интерактивный (interactive), идет прием данных, которые доступны через свойство responseText;
  • 4 - завершенный (completed), в ответ на запрос получены не только все заголовки и статус, но и приняты все данные от сервера, ответ завершен.

readonly responseText string

Ответ сервера в виде обыкновенного текста. Только чтение.

readonly responseXML object

Ответ сервера в виде объекта DOM Document. Используется, если ответ сервера является корректным XML документом. Если документ не корректный, данные не получены или еще не оправлены, то свойство равно NULL. Только чтение.

readonly status string

Статус ответа. Например: 200 (ОК), 404 (документ не найден), 503 (временная перегрузка сервера).

МЕТОДЫ:

void abort()

Прерывает HTTP запрос или получение данных. Очищает все свойства объекта, которым присваиваются начальные значения. Метод полезен в связке с таймером, когда по прошествии определенного времени с момента запроса (вылете в тайм-аут) ответ от сервера так и не был получен.

string getAllResponseHeaders()

Возвращает все заголовки ответа сервера в виде отформатированной строки. Каждый новый заголовок начинается с новой строки.

string getResponseHeader(string header)

Вернуть заголовок с именем header.

void open(string method, string uri, [boolean asynch])

Подготавливает запрос по адресу uri методом method (POST или GET) с указанием режима asynch, асинхронный режим или нет. В результате вызова свойство readyState становиться равным 1.

void send(string data)

Инициирует запрос к серверу. В запросе передаются данные data.

void setHeader(string header, string value)

Присваивает заголовку с именем header, значение value. Перед началом использования этого метода не забудьте вызвать open()!

"Как это можно использовать на своем сайте?"

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

Итак, у нас задача: нужно реализовать базу данных (БД) драйверов для различных устройств. При этом БД настолько большая, что нет смысла пересылать её приложению-клиенту и делать выборку из неё посредством JavaScript. Из-за изменения одного значения на странице перегружать её тоже нежелательно. Лично я для реализации данной задачи применяю для серверных скриптов PHP, а реализации БД применяю XML файл.

Структуру БД выбираю следующую:

<Тип_устройства>
  <Шина_подключения_к_системе>
    <Производитель>
      <Устройство операционная_система="ссылка_на_файл_драйвера">

Листинг БД файл data.xml:

<?xml version="1.0" encoding="windows-1251"?>
<Devices>
	<VideoCards>
		<AGP title="Шина">
			<Sapphire title="Производитель">
				<Device w2000="url-id1_w2000" w98="url"
                        wXP="url" freebsd="url">ATI 9600
                        128 DDR (128bit)</Device>
				<Device w2000="url-id2_w2000" w98="url"
                        wXP="url" freebsd="url">ATI 9600
                        256 DDR (128bit)</Device>
				<Device w2000="url-id3_w2000" w98="url"
                        linux="url" freebsd="url">ATI
                        9600XT 256 DDR (128bit)</Device>
				<Device w2000="url-id4_w2000" w98="url"
                        wXP="url" freebsd="url">ATI
                        X800GTO 256 DDR (256 bit)</Device>
			</Sapphire>
			<GeCube title="Производитель">
				<Device w2000="url" w98="url"
                        wXP="url" freebsd="url">ATI
                        X1300 512 DDR(128bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">ATI
                        X1300 256 DDR (128bit)</Device>
			</GeCube>
			<LeadTek title="Производитель">
				<Device w2000="url" w98="url"
                        wXP="url"  linux="url"
                        freebsd="url">NVidia 6600 128
                        DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">NVidia
                        7800GS 256 DDR (256 bit)</Device>
			</LeadTek>
		</AGP>
		<PCI-Express title="Шина">
			<Sapphire title="Производитель">
				<Device w2000="url" w98="url"
                        linux="url" wXP="url"
                        freebsd="url">ATI X1300Pro 256
                        DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">ATI
                        X1600Pro 256 DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        wXP="url" freebsd="url">ATI
                        X1800GTO 256 DDR  (256bit)</Device>
			</Sapphire>
			<ASUS title="Производитель">
				<Device w2000="url" w98="url"
                        wXP="url" freebsd="url">ATI
                        X1600Pro 256 DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">ATI
                        X1900XT 512 DDR (256bit)</Device>
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">NVidia 6600 Silencer
                        128 DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">NVidia
                        6600GT 128 DDR (128bit)</Device>
			</ASUS>
			<GigaByte title="Производитель">
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">ATI
                        X1900XT 512 DDR (256bit)</Device>
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">ATI X1900XTX 512
                        DDR (256bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">ATI
                        X800 SilentPipe 128 DDR(256bit)</Device>
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">Nvidia 6600GT 128
                        DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">NVidia
                        6600GT PassiveHeatsink 128 DDR (128bit)</Device>
			</GigaByte>
			<GeCube title="Производитель">
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">PCI-E
                        ATI X550 128 DDR (128bit)</Device>
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">PCI-E ATI X800GT Uniwise
                        256 DDR (256 bit)</Device>
				<Device w2000="url" w98="url"
                        wXP="url">ATI X800GTO 256 DDR
                        (128bit)</Device>
			</GeCube>
		</PCI-Express>
	</VideoCards>
	<SoundCards>
		<PCI title="Шина">
			<Creative title="Производитель">
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">Audigy 2 6.1</Device>
				<Device w2000="url" w98="url"
                        wXP="url">Audigy 2 ZS 7.1</Device>
				<Device w2000="url" w98="url"
                        wXP="url">X-Fi Platinum</Device>
			</Creative>
			<M-Audio title="Производитель">
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">Audiophile 192</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">Revolution
                        5.1</Device>
			</M-Audio>
		</PCI>
		<FireWire title="Шина">
			<M-Audio title="Производитель">
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">Audiophile</Device>
			</M-Audio>
		</FireWire>
		<USB title="Шина">
			<M-Audio title="Производитель">
				<Device w2000="url" w98="url"
                        linux="url"
                        freebsd="url">Audiophile</Device>
				<Device w2000="url" w98="url"
                        wXP="url" linux="url"
                        freebsd="url">Fast Track</Device>
			</M-Audio>
		</USB>
	</SoundCards>
	<Printers>
		<USB title="Шина">
			<Jet title="Струйные/Лазерные">
				<Canon title="Производитель">
					<Device w2000="url" w98="url"
                            linux="url"
                            freebsd="url">PIXMA iP 90</Device>
					<Device w2000="url" w98="url"
                            wXP="url" linux="url"
                            freebsd="url">PIXMA iP4200</Device>
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">PIXMA
                            iP6600D</Device>
				</Canon>
				<Epson title="Производитель">
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">Picture
                            Mate 100</Device>
					<Device w2000="url" w98="url"
                            wXP="url" linux="url"
                            freebsd="url">Stylus Color C48</Device>
					<Device w2000="url" w98="url"
                            wXP="url">Stylus Color C87U</Device>
				</Epson>
				<HP title="Производитель">
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">DeskJet
                            1280</Device>
					<Device w2000="url" w98="url"
                            wXP="url" linux="url"
                            freebsd="url">DeskJet 5443</Device>
					<Device w2000="url" w98="url"
                            wXP="url" linux="url"
                            freebsd="url">Photo Smart 385</Device>
				</HP>
			</Jet>
			<Laser title="Струйные/Лазерные">
				<Canon title="Производитель">
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">Laser
                            Shot LBP2900</Device>
					<Device w2000="url" w98="url"
                            wXP="url" linux="url"
                            freebsd="url">Laser Shot LBP3300</Device>
				</Canon>
				<Samsung title="Производитель">
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">ML
                            1615</Device>
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">ML
                            2015</Device>
				</Samsung>
				<HP title="Производитель">
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">LaserJet
                            1018</Device>
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">LaserJet
                            2420</Device>
					<Device w2000="url" w98="url"
                            linux="url" freebsd="url">LaserJet
                            2420DN</Device>
				</HP>
			</Laser>
		</USB>
	</Printers>
	<Scanners>
		<USB title="Шина">
			<Canon title="Производитель">
				<Device w2000="url" w98="url"
                        linux="url" wXP="url" >4200F</Device>
				<Device w2000="url" w98="url"
                        wXP="url">LiDE500F</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">LiDE60</Device>
			</Canon>
			<Epson title="Производитель">
				<Device w2000="url">Perfection 1270</Device>
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">Perfection
                        3590</Device>
				<Device w98="url">Perfection 4990</Device>
			</Epson>
			<Mustek title="Производитель">
				<Device w2000="url" wXP="url"
                        >Bear Paw 2400CU</Device>
			</Mustek>
		</USB>
		<FireWire title="Шина">
			<Epson title="Производитель">
				<Device w2000="url" w98="url"
                        linux="url" freebsd="url">Perfection
                        4990</Device>
			</Epson>
		</FireWire>
	</Scanners>
</Devices>

Как в этой БД человек ведет поиск? Скорее всего, он от корневого элемента шел бы по дереву документа до тех пока в нужной ветви не нашел ссылку или убедился, что драйвера для данного устройства нет в базе. Также поступим и мы, используя для нахождения нужного узла или набора узлов выражения языка XPath [11].

Листинг формы для отправки данных index.htm:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
<title>Драйвера</title>
<script type="text/javascript" src="ajax.js"></script>
<style type="text/css">
div {margin: 10px;}
</style>
</head>

<body>
<form name="form">
<div>
<select name="Devices" onblur="sendData(this)">
	<OPTGROUP label="Тип устройства">
	<option value="VideoCards">видео карта</option>
	<option value="SoundCards">звуковая карта</option>
	<option value="Printers">принтер</option>
	<option value="Scanners">сканер</option>
	</OPTGROUP>
</select>
</div>
<input type="hidden" name="path" value="//Devices">
<input type="hidden" name="flag" value="0">
</form>
<div id="url"></div>
<input type="button" onclick="reset()" value="Новый поиск">
</body>
</html>

В форме есть две переменные: path и flag. В первой хранится запрашиваемый путь, который отправляется на сервер. Так как один элемент в форме уже есть, то у этой переменной уже есть начальное значение. Вторая переменная служит для того, чтобы указать серверному скрипту, что из документа нужно извлечь определенный элемент Device. Кроме того, формат возвращаемых данных с сервера изменится.

Теперь рассмотрим JS-движок. Все функции клиентской части собраны в скрипте ajax.js:

y = new Object();
function httpRequest() {
	if (window.XMLHttpRequest) {
		//создание объекта для всех браузеров кроме IE
        requestObj = new XMLHttpRequest();
    } else if (window.ActiveXObject) {
    	//для IE
        requestObj = new ActiveXObject("Msxml2.XMLHTTP");
        if (!requestObj) {
            requestObj = new ActiveXObject("Microsoft.XMLHTTP");
        };
    };
};

function sendRequest (url,data) {
	httpRequest();
	//определяем call-back функцию
	requestObj.onreadystatechange = responseServer;
	//подготовка отправки данных, readyState=1
	requestObj.open('POST',url,true);
	/*
	Т.к. данные отправляются POST методом, то необходимо
	серверу отослать заголовок информирующий его об этом
	*/
	requestObj.setRequestHeader("Content-Type",
        "application/x-www-form-urlencoded; charset=UTF-8");
	//отправка данных на сервер
	requestObj.send(data);
};

function responseServer() {
    if (requestObj.readyState == 4) {
    	var status = requestObj.status;
    	if (status == 200) {
    		addSelect(y);
    	} else if (status == 400) {
    		alert('Неправильный запрос');
    	} else if (status == 500) {
    		alert('Внутреняя ошибка на сервере');
    	} else if (status == 503) {
    		var time = requestObj.getResponseHeader('Retry-After')*1000;
    		alert('Сервер перегружен.
             Запрос будет повторен через: '+time+' секунд');
    		setTimeout(sendRequest(url,path),time);
    	} else {
    		alert('Ошибочный ответ сервера');
    	};
    };
};

function sendData(obj) {
	var Elpath = document.form.path;
	var url = 'index.php';
	if (document.form.flag.value == '0') {
		var path = Elpath.value + '/' + obj.value;
	} else {
		var path = Elpath.value + '/Device["' + obj.value + '"]';
		/*
		методом GET отправляем серверному скрипту информация о том
		что необходим конкретный элемент Device
		*/
		url = 'index.php?flag=1';
	};
	//присваиваем переменной формы path значение текущего запроса
	Elpath.value = path;
	//кодируем передаваемую строку path
	path = 'path='+encodeURIComponent(path);
	y = obj;
	sendRequest (url,path);
};

function addSelect(obj) {
	//ответ сервера в виде обычного текста
	var docTEXT = requestObj.responseText;
	obj.setAttribute('disabled',true);
	//создаем элемент div
	var div = document.createElement('div');
	//добавляем ответ сервера в div
	div.innerHTML = docTEXT;
	//добавляем div с ответом сервера в дерево документа
	document.form.appendChild(div);
};

function reset() {
	document.form.path.value='//Devices';
	document.form.flag.value='0';
	var NodeListDiv = document.form.getElementsByTagName('div');
	var length = NodeListDiv.length;
	if (length > 1) {
		while (NodeListDiv[1] != undefined) {
			document.form.removeChild(NodeListDiv[1]);
		};
	};
	document.form.Devices.removeAttribute('disabled');
};

Как я уже говорил, в функцию свойства onreadystatechange нельзя передать параметры. Точнее нельзя передавать параметры, которые являются объектами. Поэтому в самом начале создаем переменную, в которой и будем хранить ссылку на вызвавший функцию объект. Поскольку данная переменная находится в глобальной зоне видимости переменных, то обратиться к ней можно будет из любой части программы. На данный момент это самый разумный способ передать параметры call-back функции свойства onreadystatechange объекта.

А теперь разберем по шагам работу движка.

При наступлении события onblur (элемент select потерял фокус) вызывается функция sendData(), которая и подготавливает POST данные для оправки запроса. Кроме того, она формирует XPath выражение в зависимости от значения переменной flag=0 (например, //Devices/VideoCards) или flag=1 (например, //Devices/VideoCards/AGP/Sapphire/Device["ATI 9600XT 256 DDR (128bit)"]).

Далее вызываем функцию sendRequest(), в которую передаем URL серверного скрипта, а также переменную типа строка, в которой содержатся готовые POST-данные. И первым делом создаем XMLHttpRequest объект, ссылку на который храним в переменной requestObj. Функция httpRequest() является кросс-браузерной, и будет работать во всех браузерах.

Когда-то обращение к функции httpRequest() я делал сразу при загрузке страницы через <body onload="httpRequest()"> и больше не создавал XMLHttpRequest объект. Но как оказалось, это работает для всех браузеров кроме IE, который каждый раз требует создавать новый объект. Поэтому вызов данной функции делается каждый раз перед отправкой данных.

После отправки данных браузер ждет ответа с сервера. При каждом изменении свойства readyState будет вызываться функция responseServer(). Если статус ответа пришел с кодом "200" (все нормально), то будет вызвана функция addSelect(), которая и добавит полученный данные в DOM текущего документа. Все браузеры будут ждать ответа от сервера. Однако по истечении некоторого времени (time-out) принудительно назначат XMLHttpRequest.readyState = 4 и перестанут ожидать ответа с сервера. Например, для Opera значение тайм-аута составляет 10 секунд. Используя другие статусы, можно добавить в движок обработчик ошибок в ответах сервера.

Функция addSelect() добавляет в DOM текущего документа еще один узел DIV, в который и помещает ответ с сервера. Может возникнуть вопрос, почему используется свойство responseText, а не responseXML? У кого-то обязательно возникнет желание, используя это свойство, импортировать ответ сервера (а серверный скрипт в ответ присылает XML документ) прямо в DOM документа. Возникло такое желание и у меня. Я хотел импортировать корневой элемент присланного XML файла и все его потомки методом importNode. Но браузер импортировал элемент без потомков, даже несмотря на то, что второй параметр данного метода был установлен в true: importNode(Object importedNode,true). Поэтому не точная реализация этого метода пока исключает его использование.

Равнозначное решение было найдено, используя innerHTML метод элемента.

На этом работа клиентской части заканчивается. Оставшаяся нерассмотренной функция reset() призвана вернуть DOM документа к начальному виду. Достичь того же можно, обновив страницу по F5, но AJAX-движок как раз и пишется для того, чтобы избежать перезагрузки страницы. Поэтому все элементы, добавленные в документ скриптом, должны быть удалены из него также скриптом.

В ответ на запрос серверный скрипт формирует XML данные вида:

<select name="ElementValue" onblur="sendData(this)">
    <optgroup label="ArrgumentTitle">
        <option value="childrenElementName_1">childrenElementName_1</option>
        ....
        <option value="childrenElementName_N">childrenElementName_1</option>
    </optgroup>
</select>

Если запрашиваемый узел имеет имя Device, то возвращается обычный отформатированный текст. Серверный скрипт написан на PHP V5 и не будет работать на более ранних версиях этого интерпретатора, так как расширение для работы с DOM было введено в этот язык только с пятой версии, и заменило собой расширение DOM XML, интерфейс которого не соответствовал спецификации. А теперь посмотрим на код серверного скрипта.

Листинг файла index.php:

<?php
$doc = new DOMDocument();
$doc->load('data.xml');

//создаем объект XPath
$DOMXPath = new DOMXPath($doc);
$DOMNodeList = $DOMXPath -> query($_POST[path]);
//согласно запросу извлекаем нужный элемент
$DOMNode = $DOMNodeList -> item(0);

//создаем объект XML-документ
$replyXML = new DOMDocument('1.0', 'windows-1251');

/*
если flag не равен единице, значит текущий элемент
не является элементом Device и необходимо найти
все элементы-потомки текущего элемента DOMNode
*/
if ($_GET[flag] != 1) {
	//получаем список все потомков элемента
	$childNodes = $DOMNode -> childNodes;

	/*
	Поскольку потомки могут быть не только элементы,
	но и узлы, то создаем индексный массив который
	содержит только элементы-потомки
	*/
	foreach ($childNodes as $Node) {
		if ($Node->nodeType == 1) {
			$arrayNodes[] = $Node;
		};
	};
	//создаем корневой элемент XML-документа
	$root = $replyXML->createElement('select');
	$optgroup = $replyXML->createElement('optgroup');

	/*
	если элементы-потомки не являются Device, то задаем атрибуты для
	корневого элемента и его элемента-потомка optgroup
	*/
	if ($arrayNodes[0] -> nodeName != 'Device') {
		$root->setAttribute('name',$DOMNode->nodeName);
		$AttributeNode = $arrayNodes[0]->getAttributeNode('title');
		$optgroup->setAttribute('label',$AttributeNode->value);
		$root->setAttribute('onblur','sendData(this)');
	} else {
	/*
	в противном случае создаем атрибут с JS кодом который и присвоит
	переменной в форме flag значение '1'
	*/
		$root->setAttribute('onblur',
            "document.form.flag.value=1;sendData(this);");
	};

	/*
	цикл создающий для каждого элемента-потомка новые элементы option;
	сколько потомков, столько и элементов
	*/
	foreach ($arrayNodes as $Node) {
		$option = $replyXML->createElement('option');
		$setNode = $Node->nodeName;
		if ($Node->nodeName == 'Device') {
			$setNode = $Node->nodeValue;
		};
		$option-> nodeValue = $setNode;
		$option->setAttribute('value',$setNode);

		$optgroup->appendChild($option);
	};

	//вставляем в XML-документ получившиеся элементы
	$replyXML->appendChild($root);
	$root->appendChild($optgroup);

/*
если flag=1, то значит текущий элемент является Device
элементом; элементы-потомки не нужны, а нужны атрибуты
текущего элемента
*/
} else {
	//создаем корневой элемент
	$root = $replyXML->createElement('pre');
	$DOMText = new DOMText("\r\nOS\t\t\tURL");

	$root -> appendChild($DOMText);

	$NamedNodeMapAttr = $DOMNode->attributes;
	$i = 0;

	/*
	цикл который находит все атрибуты элемента Device
	и для каждого атрибута создает строку с данными
	содержание ссылку
	*/
	while (($NodeAttr = $NamedNodeMapAttr->item($i)) != null) {
		if ($NodeAttr->name != 'id') {
			$DOMText = new DOMText("\r\n$NodeAttr->name\t\t\t");

			$DOMElement = $replyXML->createElement('a');
			$DOMElement -> setAttribute('href',$NodeAttr->value);
			$DOMElement -> nodeValue = $NodeAttr->value;

			$root -> appendChild($DOMText);
			$root -> appendChild($DOMElement);
		};
		$i++;
		$NodeAttr = $NamedNodeMapAttr->item($i);
	};
	$replyXML->appendChild($root);
};
//отсылаем ответ клиенту
echo $replyXML->saveHTML();
?>

Список полезной литературы:

  1. W3C. Document Object Model (DOM).
  2. Jesse James Garrett. Ajax: A New Approach to Web Applications. (перевод)
  3. W3C. HTML 4.01 Specification. (перевод)
  4. W3C. XHTML 1.0. (перевод)
  5. W3C. CSS 2 Specification. (перевод)
  6. W3C. Extensible Markup Language (XML) 1.0 (Third Edition). (перевод)
  7. Д. Лебедев. XML: спецификация и функции DOM в PHP.
  8. Mozilla Developer Center. JavaScript 1.6. (Ядро JavaScript. Справочник.)
  9. W3C. The XMLHttpRequest Object.
  10. XULPlanet. nsIXMLHttpRequest.
  11. W3C. XML Path Language (XPath) Version 1.0. (перевод)


Дополнительно

Нашли ошибку на сайте? Выделите текст и нажмите Shift+Enter

Код для блога бета

Выделите HTML-код в поле, скопируйте его в буфер и вставьте в свой блог.