xmlhack.ru logo
>> статьи на xmlhack.ru

Стратегия кэширования AJAX

Автор: Брюс Перри
Перевод: Кондаков Валерий
Опубликовано на XML.com (03.05.2006, англ.): http://www.xml.com/pub/a/2006/05/03/an-ajax-caching-strategy-prototype.html
Опубликовано на xmlhack.ru (07.06.2006, рус.): http://xmlhack.ru/texts/06/ajax-caching-strategy/ajax-caching-strategy.html
В закладки:   Del.icio.us   reddit

Возможность 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 показывает, как выглядит левая часть страницы. В левой колонке отображаются цены на энергоносители, которые подсвечиваются, когда над ними проходит курсор мыши.

Figure 1-1
Рис. 1-1. Страница отображает цены на энергоносители. (Нажмите для увеличения.)

Каждая часть тэга div на странице, посвящённая отдельной цене (например, на нефть), содержит кнопку «Обновить». Это позволяет пользователю получить самую последнюю цену. СЭИ, как бы то ни было, обновляет эти цены всего лишь раз в неделю. Поэтому было бы расточительным обрабатывать такие запросы, если пользователь загрузил страницу всего час назад. Известно, что с высокой степенью вероятности цена не изменилась, так что нам следует оставить отображаемую в данный момент цену как есть.

Определить срок кэширования данных

Всегда существует несколько работоспособных решений для данной проблемы или потребности. Мною выбранное решение заключается в том, чтобы, используя библиотеку Prototype с открытым кодом на языке JavaScript, создать объект, который отслеживает изменение цен с помощью HTTP-запроса. Объект имеет свойство, которое представляет собой период, скажем, в 24 часа. Если цена была получена менее, чем 24 часа назад, тогда объект предотвращает инициализацию новых запросов и сохраняет отображаемую цену неизменной.

В этом и состоит стратегия кэширования AJAX, созданная для периодического обновления данных с сервера в том и только том случае, если данные на стороне клиента были кэшированы на указанный срок. Если пользователь приложения не меняет страницу в браузере более 24-х часов, а затем кликает на кнопку «Обновить», инициализируется XHR и получает новую цену для отображения. Идея состоит в том, чтобы дать пользователю возможность обновлять цену без тотальной перезагрузки приложения в браузере, а также отсечь излишние запросы.

Импорт JavaScript

Веб-приложение использует тэги 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;}
      }});

}

Объект Prototype 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.

UML-диаграмма класса CacheDecider
Рис. 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



XML.com Copyright © 1998-2007 O'Reilly Media, Inc.
Перевод: xmlhack.ru Copyright © 2000-2007 xmlhack.ru