Сверхдинамичные веб-интерфейсы
Одно из главных затруднений, с которым сталкиваются разработчики интерфейсов веб-приложений, состоит в том, что после того, как страница оказалась в браузере клиента, связь браузера с сервером заканчивается. Любое действие с элементом интерфейса требует повторного обращения к серверу с повторной загрузкой новой страницы. Из-за этого веб-приложение теряет свою элегантность и медленно работает. В данной статье я расскажу о том, как данную проблему можно решить с помощью JavaScript и объекта XMLHttpRequest.
Я уверен, что вам знакома традиционная модель интерфейса веб-приложений. Пользователь запрашивает страницу с сервера, которая на сервере создается, а затем пересылается браузеру. У данной страницы есть HTML-элементы, описывающие форму, в которую пользователь вводит данные. После этого пользователь отсылает данные на сервер и получает новую страницу, основанную на введенных данных, и процесс повторяется. Весь этот процесс определяется самой природой HTTP-протокола и отличается от того, как мы работаем с обычными приложениями, интерфейс которых неразрывно связан с программной логикой.
Возьмем простой пример ввода серийного номера в каком-либо Windows-приложении. Согласно правилам, после того, как вы закончите вводить замысловатый набор цифр и букв в поля, рядом с ними появится зеленая "галочка", означающая, что вы ввели правильный номер. Она появляется моментально, как результат логики "вшитой" в интерфейс. Как только вы закончили набирать номер, программа проверяет его и выдает ответ.
В веб-интерфейсе это стандартное поведение выглядит совершенно по-другому. Разумеется, поля, в которые вы вводите серийный номер выглядят точно так же, но по завершении ввода, пользователю надо, нажав кнопку, отправить страницу на сервер, который проверит введенные данные. Обратно пользователю вернется новая страница, где будет выведено сообщение о правильном или неправильном серийном номере. Пользователю в случае неудачи надо вернуться на предыдущую страницу и снова повторить попытку. И так до бесконечности.
Разумеется не часто веб-приложение требует от пользователя ввести серийный номер, но есть бесчисленное множество других примеров, где быстрая реакция интерфейса на действия пользователя очень бы пригодилась. А так как вся программная логика находится на сервере, получить такой результат в традиционном веб-приложении весьма сложно.
На сцене появляется JavaScript
Благодаря JavaScript определенное количество программной логики можно перенести в HTML-страницу, что позволит быстро реагировать на действия пользователя. Однако у этого решения есть один главный недостаток. Первая проблема заключается в том, что как только JavaScript попадает в браузер пользователя вместе со страницей, программная логика доступна для просмотра невооруженным глазом. В случае например с проверкой правильности введенного адреса e-mail это может быть и не страшно, но если проверка связана с серийным номером, алгоритм проверки становится доступным всем, кто скачал страницу, а это неприемлемо.
Вторая проблема заключается в том, что серьезную программную логику в страницу поместить невозможно, так как интерфейс просто не предназначен для этого. Вся логика должна находиться на уровне приложения, а не пользовательского интерфейса, а это значит - мы опять возвращаемся на сервер. Проблема дополняется еще тем, что не всегда с уверенностью можно ожидать наличия JavaScript в браузере клиента. В то время, как большинство пользователей оставляют поддержку JavaScript в своих браузерах включенной, существует значительное количество пользователей, которые этого не делают, или пользуются таким браузером, где JavaScript отсутствует или вообще не нужен как класс. Следовательно всю логику, которую делает JavaScript на стороне клиента, все равно придется проверять на сервере на всякий случай.
Объект XMLHttpRequest
Решением этой проблемы может стать объект XMLHttpRequest. Этот объект впервые был реализован компанией Microsoft в виде объекта ActiveX, но сейчас он доступен как встроенный объект во всех браузерах Mozilla и Safari. Этот объект позволяет JavaScript-у осуществлять HTTP-запросы к удаленному серверу без необходимости перезагружать страницу. По сути HTTP-запросы отправляются и получаются полностью за "кулисами" страницы, а пользователь их даже не замечает.
Это огромный шаг вперед, так как позволяет разработчику веб-приложения достичь той самой желанной цели - создание быстрого пользовательского интерфейса с сохранением при этом программной логики на сервере. С помощью JavaScript-а данные, введенные пользователем, отправляются на сервер, там они обрабатываются и пользователь практически тут же получает ответ на введенные данные.
Начнем с основ
Из-за своей противоречивой истории объект XMLHttpRequest еще не является частью какого-либо стандарта (хотя нечто подобное уже было предложено в спецификации W3C DOM Level 3 Load and Save). Поэтому существует два отличных друг от друга метода вызова этого объекта в коде скрипта. В Internet Explorer объект ActiveX вызывается так:
var req = new ActiveXObject("Microsoft.XMLHTTP");
В Mozilla и Safari это делается проще (так как там это объект, встроенный в JavaScript):
var req = new XMLHttpRequest();
Разумеется из-за таких различий вам необходимо создавать в коде ветки, каждая из которых будет выполняться в зависимости от того, в каком браузере загружен скрипт. Существует несколько способов, как сделать это (включая различные мудрёные хаки и метод "условных комментариев"). Но я считаю, что лучше всего просто проверять в коде, поддерживается ли браузером тот или иной объект. Хорошим примером может служить код, взятый с сайта Apple, где выложена документация по этому объекту. Давайте им и будем пользоваться:
var req; function loadXMLDoc(url) { // branch for native XMLHttpRequest object if (window.XMLHttpRequest) { req = new XMLHttpRequest(); req.onreadystatechange = processReqChange; req.open("GET", url, true); req.send(null); // branch for IE/Windows ActiveX version } else if (window.ActiveXObject) { req = new ActiveXObject("Microsoft.XMLHTTP"); if (req) { req.onreadystatechange = processReqChange; req.open("GET", url, true); req.send(); } } }
В этом коде особенно важно обратить внимание на свойство onreadystatechange. Посмотрите, как ему присваивается значение функции processReqChange. Это свойство - хендлер события, которое запускается всякий раз, когда меняется состояние объекта req. Состояния обозначаются номерами с 0 (объект неинициализирован) по 4 (запрос выполнен). Важно это потому, что наш скрипт не будет ждать ответа от сервера, чтобы продолжить свою работу. HTTP-запрос будет сформирован и отослан на сервер, но скрипт будет выполняться дальше. Из-за того, что мы выбрали такой вариант поведения, нам нельзя просто в конце функции вернуть результат запроса, так как нам неизвестно, получили мы его к этому времени или нет. Для этого мы и предусмотрели функцию processReqChange, которая будет отслеживать состояние объекта req, и сообщит нам в нужное время, что процесс получения документа закончен, и мы можем идти дальше.
Для этого функции processReqChange требуется проверять две вещи. Первая - ждать, когда состояние объекта req изменится на 4 (означающее, что процесс получения документа с сервера закончен). Второе, это проверить HTTP-статус ответа. Вы знаете, что код 404 означает "файл не найден" и 500 - "произошла ошибка на сервере". Но нам нужен старый добрый код 200 ("все ОК"), который означает, что на сервере наш запрос был успешно выполнен. Если мы получили и состояние 4 и код 200, мы можем продолжать выполнение нашего скрипта и обрабатывать результаты, полученные от сервера. Разумеется в противном случае мы должны обработать все ошибки, например, если код ответа отличается от 200.
function processReqChange() { // only if req shows "complete" if (req.readyState == 4) { // only if "OK" if (req.status == 200) { // ...processing statements go here... } else { alert("There was a problem retrieving the XML data:\n" + req.statusText); } } }
На практике
Я собираюсь создать работающий пример для демонстрации вышеизложенных идей. Во многих веб-приложениях есть процедура, когда новый пользователь регистрируется на сервере и требуется выбрать "ник" для регистрации. Очень часто этот "ник" должен быть уникальным, и потому после того, как пользователь выбрал себе "ник", на сервере осуществляется проверка по базе данных пользователей, есть уже такой "ник" или нет. Если вы когда-либо регистрировались на каком-нибудь почтовом веб-сервере, вы помните, как утомительно подыскивать "ник", которым еще никто не пользуется. Было бы очень хорошо, если бы проверка осуществлялась без необходимости всякий раз обновлять страницу.
Для решения мы воспользуемся четырьмя ключевыми элементами: XHTML-формой, функцией JavaScript, специально написанной для данной ситуации, двумя нашими общими функциями, о которых мы говорили выше, и наконец серверным скриптом, который будет обращаться к базе данных.
Форма
Это самая легкая часть работы - простая форма с полем для ввода "ника". К событию onblur мы привязываем наш скрипт проверки. Для того, чтобы вывести пользователю сообщение о результатах проверки, я вставил его в форму и спрятал с помощью CSS. Это более элегантный и вежливый способ, чем стандартное диалоговое окно функции alert().
<input id="username" name="username" type="text" onblur="checkName(this.value,'')" /> <span class="hidden" id="nameCheckFailed"> This name is in use, please try another. </span>
В CSS объявлены свойства класса hidden, а также еще одного класса error, который служит для вывода сообщений об ошибке.
span.hidden{ display: none; } span.error{ display: inline; color: black; background-color: pink; }
Обработка введенных данных
Функция checkName служит для проверки данных, введенных пользователем форму. Задача функции - взять данные, решить, какому серверному скрипту эти данные отдать, вызвать другую функцию, которая сделает всю "грязную" работу с HTTP-запросами и ответами, и затем обработать ответ. Эта наша функция будет состоять из двух частей. Одна часть - берет данные из формы, а другая - обрабатывает ответ от сервера. Я объясню, зачем это сделано, чуть позднее.
function checkName(input, response) { if (response != ''){ // Response mode message = document.getElementById('nameCheckFailed'); if (response == '1'){ message.className = 'error'; }else{ message.className = 'hidden'; } }else{ // Input mode url = 'http://localhost/xml/checkUserName.php?q=' \\ + input; loadXMLDoc(url); } }
Ответ обрабатывается просто - ответ, который мы получим от серверного скрипта, будет либо 1, либо 0. 1 означает, что такой "ник" уже кем-то используется. В зависимости от ответа наша функция меняет название класса сообщения об ошибке - оно либо показывается, либо прячется. Как понятно из кода, работу на сервере выполняет скрипт checkUserName.php.
Формирование HTTP-запроса и ответа
Как вы видели выше, работа с HTTP выполняется двумя функциями: loadXMLDoc и processReqChange. В первом практически ничего менять не надо, а во втором требуется кое-что поменять для работы с DOM-ом.
Как вы помните, пока успешный результат выполнения запроса не будет передан функции processReqChange, мы не можем из этой функции вернуть какое-либо значение. Из-за этого нам требуется выполнять явный вызов функции из другого места кода, чтобы воспользоваться результатом. Вот почему функция checkName разбита на две части. Таким образом, главная задача функции processReqChange состоит в том, чтобы обработать XML-ответ, полученный от сервера, и передать определенное значение из этого ответа функции checkName.
Мы не хотим вносить в эти функции какой-либо специфический код, так как эти функции могут использоваться и другими элементами страницы для обращения к серверу. Поэтому мы и не вписывали в функцию processReqChange имя функции checkName. Вместо этого мы решили, что сервер сам будет в своем ответе давать название функции, которая к нему обращалась.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <response> <method>checkName</method> <result>1</result> </response>
Разбор этого простого ответа выполняется без каких-либо проблем.
function processReqChange() { // only if req shows "complete" if (req.readyState == 4) { // only if "OK" if (req.status == 200) { // ...processing statements go here... response = req.responseXML.documentElement; method = response.getElementsByTagName('method') \\ [0].firstChild.data; result = response.getElementsByTagName('result') \\ [0].firstChild.data; eval(method + '(\'\', result)'); } else { alert("There was a problem retrieving \\ the XML data:\n" + req.statusText); } } }
Обратившись к свойству responseXML объекта XMLHttpRequest, мы получаем XML-ответ, пришедший от сервера, который мы затем разбираем с помощью DOM. Взяв из ответа название функции, которая требовала этот ответ, мы знаем, какой функции надо передать полученное значение. После того, как вы закончите тестирование, уберите условие else, чтобы скрипт не беспокоил пользователей лишним сообщением об ошибке.
Серверный скрипт
Последний элемент нашей мозаики - это серверный скрипт, который будет получать запрос, обрабатывать его и возвращать ответ в виде XML. В нашем примере скрипт будет обращаться к базе данных пользователей, чтобы определить, используется ли уже данный "ник" или нет. Для краткости в моем примере скрипт не использует базу данных, а просто проверяет два имени "Drew" и "Fred".
<?php header('Content-Type: text/xml'); function nameInUse($q) { if (isset($q)){ switch(strtolower($q)) { case 'drew' : return '1'; break; case 'fred' : return '1'; break; default: return '0'; } }else{ return '0'; } } ?> <?php echo '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'; ?> <response> <method>checkName</method> <result><?php echo nameInUse($_GET['q']) ?> </result> </response>
Разумеется, ту же процедуру проверки надо предусмотреть и в том серверном скрипте, который будет выполняться, когда пользователь нажмет кнопку "Submit" на форме регистрации. Это на тот случай, если у него в браузере был отключен JavaScript, или пользователь намеренно ввел "ник", который уже используется. Кроме того на загруженных сайтах может так случиться, что во время выбора "ника" он был свободен, а на момент подачи его уже кто-то занял.
В качестве следующего шага расширьте сами функциональность скрипта. Например сервер может возвращать варианты альтернативных "ников" на основе "ника", выбранного пользователем, но уже занятого.
В заключение
Этот небольшой пример лишь слегка касается темы использования объекта XMLHttpRequest. Вот лишь некоторые примеры его использования: Google Suggest, где XMLHttpRequest используется для того, чтобы подсказать искомые слова, приложение Ta-da Lists, где данные подаются на сервере опять-таки с помощью XMLHttpRequest благодаря чему интерфейс работает очень быстро. Самое интересное здесь не том, чтобы заставить код работать, а в том, чтобы найти еще какие-либо интересные способы его использования.
Drew McLellan
Drew is a web application developer and technical author from just West of London, UK. A keen standards advocate, Drew helps out at the Web Standards Project in addition to maintaining a personal web site at allinthehead.com. When he grows up, he'd like to be a helicopter pilot.
Оставить комментарий
Комментарии
do_something();
}
1. FireFox выдавал ошибку:
до тех пор, пока я не убрал двойной разрыв строки между следующими двумя строками в файле php:
в статье было (выдает ошибку)
<?php echo '<?xml version="1.0" encoding="UTF-8"
я исправил на (ошибка исчезла)
<?php echo '<?xml version="1.0" encoding="UTF-8"
2. Почему-то отказывалось работать условие:
message.className = 'error';
}else{
message.className = 'hidden';
}
заработало следующее:
message.className = 'error';
}else{
message.className = 'hidden';
}
Это очень странно, потому что при проверке typeof response выводится string, а получается, что для работы кода требуется сравнение с числом...