Возможность AJAX-приложений негласно
создавать HTTP-подключения для получения небольших порций информации
является одним из самых мощных инструментов, включаемых в API современных
браузеров. Для большинства браузеров (например, Firefox 1.5, Safari 2.0,
Opera 8.5) механизмом, выполняющим данную функцию, является объект
XMLHttpRequest
(XHR). Internet Explorer 5.5-6 имеет иной
синтаксис для данного объекта:
XMLHttp
, однако предполагается, что IE 7 адаптирует этот объект
к синтаксису других браузеров.
Однако, создание HTTP-запросов из приложений AJAX по причине чистой необходимости никогда не было хорошей идеей или замыслом. Серверная сторона в таком взаимодействии может не справиться с потоком запросов. Запросы клиентской стороны AJAX-приложения могут быть отвергнуты или вообще не пройти из-за превышения времени отклика, что подорвёт доверие пользователя к силе AJAX.
Приложение, которое я представил в недавней статье, использует XHR для получения текущих цен на нефть, бензин и другие энергоносители. Эти числа, предоставленные Службой Энергетической Информации США (СЭИ), находятся в общем доступе. Моё приложение собирает (или «выдирает») их с соответствующих сайтов. Было бы лучше соединяться с энергетическим веб-сервисом (сбор информации из HTML неуклюж и нестабилен), но мне не удалось найти открытого ресурса с бесплатным доступом. Жду ваших рекомендаций!
Страницы СЭИ удовлетворяют целям приложения, которое, в конце концов, предназначено для того, чтобы поставлять торговцам нефтяными фьючерсами вторичную информацию. Приложение отображает цены в окне браузера, когда пользователь загружает страницу. Рис. 1-1 показывает, как выглядит левая часть страницы. В левой колонке отображаются цены на энергоносители, которые подсвечиваются, когда над ними проходит курсор мыши.
Каждая часть тэга
div
на странице, посвящённая отдельной цене (например, на
нефть), содержит кнопку «Обновить». Это позволяет пользователю получить самую
последнюю цену. СЭИ, как бы то ни было, обновляет эти цены всего лишь раз в
неделю. Поэтому было бы расточительным обрабатывать такие запросы, если
пользователь загрузил страницу всего час назад. Известно, что с высокой
степенью вероятности цена не изменилась, так что нам следует оставить
отображаемую в данный момент цену как есть.
Всегда существует несколько работоспособных решений для данной проблемы или потребности. Мною выбранное решение заключается в том, чтобы, используя библиотеку Prototype с открытым кодом на языке JavaScript, создать объект, который отслеживает изменение цен с помощью HTTP-запроса. Объект имеет свойство, которое представляет собой период, скажем, в 24 часа. Если цена была получена менее, чем 24 часа назад, тогда объект предотвращает инициализацию новых запросов и сохраняет отображаемую цену неизменной.
В этом и состоит стратегия кэширования AJAX, созданная для периодического обновления данных с сервера в том и только том случае, если данные на стороне клиента были кэшированы на указанный срок. Если пользователь приложения не меняет страницу в браузере более 24-х часов, а затем кликает на кнопку «Обновить», инициализируется XHR и получает новую цену для отображения. Идея состоит в том, чтобы дать пользователю возможность обновлять цену без тотальной перезагрузки приложения в браузере, а также отсечь излишние запросы.
Веб-приложение использует тэги
script
в HTML для импорта необходимого JavaScript, включая
библиотеку Prototype. Первая мною написанная для xml.com статья об этом
приложении показывает, как скачивать и устанавливать Prototype.
<head>...
<script src="http://www.eeviewpoint.com/js/prototype.js"
type="text/javascript">
</script>
<script src="http://www.eeviewpoint.com/js/eevpapp.js"
type="text/javascript">
</script>
..</head>
Сперва я приведу здесь код с eevapp.js, который я использовал для получения цен на энергоносители. Зтем продемонстрирую объект, который выступает в роли фильтра запросов.
//Когда браузер закончил загрузку страницы,
//получить и отобразить цены на энергоносители
window.onload=function(){
// метод generatePrice использует два параметра:
//тип энергоносителя (например, oil) и id
//элемента span, в который следует отобразить цену
generatePrice("oil","oilprice");
generatePrice("fuel","fuelprice");
generatePrice("propane",
"propaneprice");
//установить кнопки «Обновить» для того,
//чтобы получить обновлённые цены на нефть,
//бензин и пропан.
//Use their onclick event handlers.
$("refresh_oil").onclick=function(){
generatePrice("oil","oilprice"); }
$("refresh_fuel").onclick=function(){
generatePrice("fuel","fuelprice"); }
$("refresh_propane").onclick=function(){
generatePrice("propane",
"propaneprice"); }
};
Комментарии в коде поясняют происходящее. Несколько странным является
синтаксический сегмент
$("refresh_oil").onclick
. Следует его здесь пояснить.
Prototype содержит функцию
$()
, возвращающую объект Document Object Model (DOM)
Element
. Этот
Element
ассоциирован с HTML-атрибутом
id
, который код передаёт функции
$()
в качестве параметра
string
. Этот синтаксис эвивалентен записи
document.getElementById("refresh_oil")
, однако короче её и
потому более удобен для разработчика. К чему относится
"refresh_oil"
? Это
id
кнопки «Обновить» на странице. См. на рис. 1-1 левую колонку
на скриншоте браузера. На ней расположена кнопка «Обновить».
<input type="submit" id="refresh_oil" .../>
Код связывает обработчик события
onclick
этой кнопки с функцией, которая опционально забирает
новую цену на энергоноситель. Опционально в том смысле, что фильтр сам
решает, следует ли инициализировать HTTP-запрос.
CacheDecider
Функция
generatePrice()
первым делом с помощью объекта
CacheDecider
, связанного с кэшированием, пытается узнать, может
ли быть инициализирован новый HTTP-запрос на цену энергоносителя. Каждая
категория энергоносителей — нефть, бензин, пропан — имеет свой собственный
объект
CacheDecider
. В коде эти объекты хранятся в хэше
cacheHash
. Таким образом, выражение
cacheHash["oil"]
возвратит объект
CacheDecider
для цен на нефть.
Если
CacheDecider
или фильтр позволит коду инициализировать новый
HTTP-запрос (
CacheDecider.keepData()
вернёт
false
), тогда код вызовет
getEnergyPrice()
для обновления, скажем, неподтверждённой цены
на нефть. Комментарии в коде объясняют, что именно происходит.
/* Проверим объект CacheDecider для выяснения, следует ли получить новую цену на энергоноситель */ function generatePrice(energyType, elementID){ // установим кэшер локальных переменных на объект CacheDecider, // ассоциированный с нефтью, бензином или пропаном var cacher = cacheHash[energyType]; //Если этот объект null, тогда объект CacheDecider //ещё не был реализован для указанного типа //энергоносителя. if(! cacher) { //CacheDecider имеет параметр, который //определяет число секунд для хранения //данных, здесь 24 часа cacher = new CacheDecider(60*60*24); //сохранить новый объект в хэше с //типом энергоносителя, скажем, "oil", в качестве ключа cacheHash[energyType]=cacher; //Получить и отобразить новую цену на энергоноситель getEnergyPrice(energyType, elementID); return; } //CacheDecider уже был реализован, поэтому //проверим, устарела ли в данный момент отображаемая //цена на энергоноситель, не следует ли её обновить. if(! cacher.keepData()) { getEnergyPrice(energyType, elementID);} } /* Использование объекта Prototype Ajax.Request для получения цены на энергоноситель. Параметры:
energyType есть oil, fuel, или propane.elementID
есть id элемента span на странице; в span будет выведена обновлённая информация. */ function getEnergyPrice(energyType, elementID){ new Ajax.Request(go_url, {method: "get", parameters: "priceTyp="+energyType, onLoading:function(){ $(elementID).innerHTML= "waiting...";}, onComplete:function(request){ if(request.status != 200) { $(elementID).innerHTML="Unavailable." } else { $(elementID).innerHTML= request.responseText;} }}); }
Ajax
Связанный с AJAX код с использованием объекта Prototype
Ajax.Request
занимает всего лишь десять строк. Код соединяется с
сервером, получает новую цену на энергоноситель от правительства США, а затем
отображает её без изменений любой другой части страницы. Как бы то ни было,
код всё же немного смущает. Вот объяснение.
Вы, возможно, видели уже, как программируется объект
XMLHttpRequest
непосредственным образом. Однако, Prototype
реализует этот объект, обеспечивая корректную работу в различных браузерах.
Простое создание нового объекта AJAX, как, к примеру, с помощью
Ajax.Request
, уже подготовит необходимый HTTP-запрос. Файл
prototype.js включает определение данного объекта. Поскольку
страница импортирует этот файл JavaScript, мы можем создавать новые
экземпляры данного объекта, не прибегая к никаким другим приёмам. В коде не
требуется учитывать различия между браузерами в отношении XHR.
Первым параметром
Ajax.Request
является URL серверной компоненты, с которой
следует установить соединение. Например,
http://www.eeviewpoint.com/energy.jsp. Второй параметр определяет
HTTP-запрос как запрос вида
GET
. Третий параметр предоставляет строку запроса, которая будет
передана серверной компоненте. Таким образом, URL мог бы выглядеть, например,
так:
http://www.eeviewpoint.com/energy.jsp?priceTyp=oil.
Часть других параметров позволяет разработчику решить, что должно делать
приложение на различных этапах обработки запроса. Например,
onLoaded
представляет состояние обработки запроса во время
вызова метода
XMLHttpRequest.send()
, к тому же доступны некоторые заголовки
ответа HTTP, так же, как и статус HTTP-ответа. На данном этапе приложение
отображает текст «waiting...», означающий, что новая цена скоро будет
доступна.
onComplete
представляет собой этап, на котором обработка запроса
касательно цены на энергоноситель завершена. Функция, ассоциированная с
onComplete
, таким образом, определяет, что должно произойти, как
только запрос возвратит значение. Код автоматически предоставляет функции
параметр запроса, представляющий собой экземпляр XHR, использованный для
HTTP-запроса.
onComplete:function(request){...
Таким образом, в данном случае код получает возвращаемое значение с
помощью синтаксиса
request.responseText
. Если всё в порядке, это свойство будет
иметь значение новой цены, например, 66.07.
Объект
Ajax.Request
так же позволяет осуществить действие, основанное
на конкретных значениях статуса HTTP-ответа. Этого можно достичь с помощью
проверки свойства XHR (
request.status
). Если значение статуса не равно
200
(например,
404
, что означает «Not Found»), значению, выражающему, что
запрос был выполнен успешно, то приложение отобразит «unavailable» в том
месте, где должна была появиться цена.
Нам следует изменить этот сценарий, чтобы в результате неудачного запроса отображаемое значение цены не изменилось.
new Ajax.Request(go_url, {method: "get",
parameters: "priceTyp="+energyType,
onLoading:function(){ $(elementID).innerHTML=
waiting...";},
onComplete:function(request){
if(request.status == 200) {
$(elementID).innerHTML=
request.responseText;}
}});
CacheDecider
Последним кусочком решения задачки является объект
CacheDecider
object. Рис. 1-2 представляет собой UML-диаграмму,
изображающую данный объект. Я написал этот объект на JavaScript, используя
библиотеку Prototype.
Рис. 1-2. Диаграмма классов, изображающая объект
CacheDecider
Вот код объекта, содержащийся в файле eevapp.js.
var CacheDecider=Class.create();
CacheDecider.prototype = {
initialize: function(rng){
//Как долго следует
//хранить ассоциированные данные, в секундах?
this.secondsToCache=rng;
this.lastFetched=new Date();
},
keepData: function(){
var now = new Date();
//Получить разницу в секундах между текущим временем
//и последним значением свойства lastFetched
var secondsDif = parseInt((now.getTime() -
this.lastFetched.getTime()) / 1000);
//Если предыдущее вычисленное значение меньше, чем
//заданное число секунд для хранения кэшированного значения,
//вернуть true
if (secondsDif < this.secondsToCache) { return true;}
//данные в кэше слудет обновить или получить заново, и, следовательно,
//сбросить значение lastFetched, приравняв его к текущему времени
this.lastFetched=new Date();
return false;
}
}
Файл
prototype.js содержит определение объекта для
Class
. Вызов
Class.create()
возвращает объект с методом
initialize()
. Словно метод конструктора в других языках,
JavaScript вызовет этот метод каждый раз, когда в коде потребуется создать
новый экземпляр ассоциированного объекта.
Метод
initialize()
для
CacheDecider
устанавливает два свойства:
secondsToCache
, которое представляет собой число секунд,
отпущенное на хранение значения в кэше до того, как должно быть получено
другое значение; и
lastFetched
, объект JavaScript
Date
, представляющий собой время последнего обновления
кэшированного значения, чем бы оно ни являлось.
CacheDecider
не хранит ссылку на кэшированное значение; зато
другие объекты используют
CacheDecider
для хранения и проверки временных границ.
Если ещё не всё ясно, см. объяснение под подзаголовком «Сперва
присмотритесь к объекту
CacheDecider
».
CacheDecider
также имеет метод
keepData()
. Этот метод определяет, превысило ли число секунд,
прошедшее с момента
lastFetched
, число секунд, которое требуется хранить
кэшированное значение.
В данном приложении значение цены на энергоноситель хранится 24 часа, пока не будет обновлено с помощью HTTP-запроса.
cacher = new CacheDecider(60*60*24);
Если это число секунд не было превышено, тогда
keepData()
возвращает true. В ином случае свойство
lastFetched
будет сброшено и приравнено к текущему времени
(поскольку кэшированное значение может быть изменено с помощью HTTP-запроса),
и метод вернёт
false
.
Пользователь может обновить данные когда пожелает, если обновит страницу целиком. Это действие инициализирует заново все объекты, которые мы обсудили.
Данное приложение использует нечто вроде «междоменного прокси», как описывается на следующем информативном сайте: http://ajaxpatterns.org/Cross-Domain_Proxy. И, хотя моё приложение получает информацию со страниц СЭИ США, сбор информации выполняется компонентом, который был взят с домена ( eeviewpoint.com), на котором размещено и данное приложение.
Серверная компонента может быть написана на любом языке, годном для сбора
информации с веб-страниц: PHP, Ruby, Python, ASP.NET, Perl, Java. Я выбрал
Java, будучи неравнодушным к программированию servlet/JSP и контейнерам
Tomcat. Объект
Ajax.Request
соединяется с JSP, который использует
вспомогательные классы Java для сбора информации касательно энергоносителей с
сайтов СЭИ США. См. «Ресурсы», чтобы узнать, где можно скачать мной
использованные классы Java.
Prototype:
http://script.aculo.us/
Код JavaScript:
http://www.parkerriver.com/ajaxhacks/xml_article_js.zip
Код Java:
http://www.parkerriver.com/ajaxhacks/xml_article_java.zip