
Ajax в действии
.pdf
552 Часть IV. Ajaxв примерах
ление — это HTML-код, наш класс представления отвечает за генерацию HTML-страницы. Итак, рассмотрим для начала конструктор, представленный в листинге 13.25-
Листинг 13.25. Класс представления RSSItemView
RSSItemView = C l a s s . c r e a t e ( ) ; |
|
|
RSSItemView.prototype = { |
|
|
i n i t i a l i z e : function(rssltem, |
feedlndex, itemlndex, numFeeds) { |
|
t h i s . r s s l t e m = |
rssltem; |
|
t h i s . f e e d l n d e x |
• feedlndex |
+ 1; |
t h i s . i t e m l n d e x = i t e m l n d e x + 1; |
||
t h i s . n u m F e e d s |
= numFeeds; |
|
Ь
}
Давайте внимательно рассмотрим параметры. Первый параметр — экземпляр RSSltem. С его помощью мы сообщаем представлению, для какого экземпляра модели обеспечивается это представление. Обратите внимание на то, что в общем случае классам модели не разумно знать что-либо о представлении, однако представление по необходимости имеет детальные знания о модели. Другие параметры обеспечивают некоторый дополнительный контекст для представления. Параметр feedlndex сообщает представлению номер ленты. Параметр itemlndex указывает представлению, где размещается данная статья в родительском массиве статей RSSFeed. Параметр numFeeds сообщает представлению, сколько всего лент. Все указанные параметры необходимы для получения информации о месте данного представления в мире. Представление может пожелать отобразить область содержимого, указывающую, например, "это лента 1 из 7 и статья 3 из 5". Данные атрибуты можно внедрить в модель, однако в действительности они не являются атрибутами, за которые в общем случае должна отвечать модель, поэтому этот контекст (требуемые представлению) передается клиентом в конструктор представления.
Как отмечалось выше, представление отвечает за генерацию HTML-кода. Поэтому нашему классу представления понадобится еще один метод. Посмотрим, как это он выглядеть (листинг 13.26).
Листинг 13.26. Метод генерации HTML-кода
toHTML: function () { var out = ""
out += '<span class="rssFeedTitlePrompt">RSS Feed '
out += '(' + this.feedlndex + • of ' + this.numFeeds + ') : '; out += '</span>';
out += '<span class="rssFeedTitle">';
out += '<a href="' + this.rssltem.rssFeed.link + '">' + this.rssltem.rssFeed.title + '</a>';
out += '</span>'; out += '<br/>';
out += '<span class="rssFeedItemTitlePrompt">Article '; out += '(' + this.itemlndex + ' of * + this.rssltem.rssFeed.items.length + ') : ' ;
out += '</span>';

Глава 13. Создание приложений Ajax, не использующих сервер 553
Рис. 13.15. RSS-лента (х из у): название RSS-ленты
o u t + = ' < s p a n c l a s s = " r s s F e e d I t e m T i t l e " > ' ;
o u t += ' < a h r e f - " 1 + t h i s . r s s l t e m . l i n k + ' " > ' + t h i s . r s s l t e m . t i t l e + ' < / a > ' ;
o u t + = ' < / s p a n > ' ;
o u t + = ' < d i v c l a s s = " r s s I t e m C o n t e n t " > ' ; o u t + - t h i s . r s s l t e m . d e s c r i p t i o n ;
o u t + = ' < / d i v > ' ;
r e t u r n o u t ;
Ь
Метод toHTML создает на экране контекстные элементы, за которыми сле-
дует текст статьи. Первый фрагмент кода отображает текст RSS-лента (х |
из |
у) : Заголовок RSS-ленты. Атрибут link родительского элемента rssFeed |
ис- |
пользуется для генерации атрибута HREF анкера, а атрибут t i t l e — для генерации текста анкера. Для каждого элемента span генерируется имя класса CSS (одно для запроса, второе для анкера), что позволяет независимо определять стили этих элементов. Сказанное иллюстрируется на рис. 13.15.
Следующий фрагмент кода генерирует текст Статья (х из у) : Заголовок RSS-статьи. Для генерации атрибута HREF анкера используется атрибут link RSS-статьи, а с помощью атрибута t i t l e статьи генерируется текст анкера. Кроме того, как показано на рис. 13.16, код предоставляет имена классов CSS для запроса и заголовка. Несколько последних строк метода toHTML генерируют элемент div, который будет вмещать содержимое статьи RSSltem (атрибут description). Соответствующий код выглядит следующим образом:
out += '<div class="rssItemContent">'; out += this.rssltem.description;
out += '</div>';
Для текста статьи генерируется имя класса CSS rssltemContent. Чтобы текст не подходил вплотную к границе блока, в соответствующем классе необходимо задать небольшие поля и заполнение. Кроме того, данный блок должен иметь фиксированную высоту и значение auto параметра overflow, чтобы его содержимое можно было при необходимости прокручивать независимо от показанной ранее контекстной информации. Типичное определение класса CSS для блока текста статьи приводится ниже.

554 Часть IV. Ajax в примерах
Рис. 13.16. Статья (х из у) заголовок RSS-статьи
Рис. 13.17. Статья (х из у) заголовок RSS-статьи
.rssItemContent {
border-top : lpx solid black; width : 590px;
height : 350px; overflow : auto; padding : 5px; margin-top : 2px;
}
При таком стилевом оформлении область содержимого должна выглядеть примерно так, как показано на рис. 13.17. Собирая вместе все составляющие, получаем показанное на рис. 13.18 представление, генерируемое классом RSSitemView. Прежде чем завершить рассмотрение представления, добавим в него еще один маленький метод, который немного облегчит его использование.
toString: function() { return this.toHTMLO;
J

RSSItemView Result
Рис. 13.18. Окончательный вид класса RSSItemView
Причина, по которой мы дали представлению метод tostring, заключается в том, что это позволяет нам взаимозаменяемо использовать экземпляр представления и генерируемую им строку HTML. Например, мы можем присвоить значение атрибуту innerHTML элемента, равное классу представления, и далее будет использоваться строковое представление (сгенерированный HTML-файл). Например, ниже приводится код, присваивающий сгенерированный HTML-документ представления атрибуту innerHTML элемента div с идентификатором contentDiv.
var rssItemView - |
new RSSItemView( anRSSFeed, 0, 0, 5 ); |
$('contentDiv1 ). |
innerHTML = rssItemView; |
(Помните, $() — это функция из библиотеки Prototype, позволяющая извлекать элементы DOM по их идентификаторам.) Теперь, когда у нас есть хороший набор абстракций для классов модели и представления, рассмотрим контроллер приложения, который свяжет воедино все эти компоненты.
13.7.3. Контроллер приложения
Класс RSSReader будет объединять функции, связанные с манипуляцией классами модели и представления, координируя все действия, соотнесенные с приложением чтения RSS-лент. Напомним, что интерфейс приложения реализован в виде слайд-шоу лент, когда каждая статья будет отображаться на определенный период времени, а затем постепенно переходить в следующую
Глава 13. Создание приложений Ajax, не использующих сервер 557
рые он должен будет определить в структуре DOM. Реализация сказанного будет показана ниже, в методе applyButtonBehaviors. Первое, что делает конструктор О, — это устанавливает значения по умолчанию для состояния. Далее (как и в большинстве других проектов) используется объект опций ©, с помощью которого задаются конфигурационные опции компонента. Для этого применяется метод setoptions. Наконец, в методе star t © происходит все, что требуется для активизации компонента. Далее мы сначала рассмотрим конфигурацию, а затем перейдем к поведению.
За конфигурацию отвечает идиома setoptions, показанная в листинге 13.28. Давайте разберемся с реализацией setoptions и обсудим опции конфигурации.
Листинг 13.28. Метод setoptions |
j j j l |
||
setOptions: function(options) |
{ |
||
|
t h i s . o p t i o n s = { |
|
|
|
slideTransitionDelay: 7000, |
||
|
fadeDuration |
: 300, |
|
|
errorHTML : |
'<hr/>Error retrievin g content.<br/>' |
|
|
}.extend(options); |
|
|
ь |
|
|
ш |
|
|
|
В приведенном методе setOptions указаны свойства приложения, которые мы решили сделать настраиваемыми. Свойство slideTransitionDelay задает число миллисекунд, в течение которых один "слайд" статьи виден до перехода во второй. Свойство fadeDuration задает время в миллисекундах, требуемое для полного затухания "слайда" (а следовательно, для "проявления" следующего "слайда"). Наконец, при возникновении ошибки в процессе загрузки RSS-ленты используется свойство errorHTML, задающее HTMLдокумент, отображаемый в качестве сообщения об ошибке. В приведенном коде также показаны значения данных свойств по умолчанию, которые сохраняются, если пользователь явно не укажет другие. Здесь стоит отметить, что компонент ожидает, что ему в качестве исходного набора лент для обработки будет передано свойство rssFeeds объекта опций. Поскольку мы в действительности не можем предложить разумное значение по умолчанию для этой величины, в методе setoptions она не определяется. Подразумевается, что приложение будет создано с объектом опций, подобным показанному ниже.
var options = |
{ |
rssFeeds: [ |
"http://radio.javaranch.com/news/rss.xml", |
|
"http://radio.j avaranch.com/pascarello/rss.xml", |
|
"http://radio.javaranch.com/bear/rss.xml", |
|
"http://radio.javaranch.com/lasse/rss.xml" ] }; |
var rssReader |
= new RSSReader{'rssReader', options ); |
Итак, мы довольно быстро закодировали создание и настройку конфигурации. Теперь можно переходить к магическому методу start, который и запустит весь процесс. В следующих разделах мы кратко рассмотрим этот метод и посмотрим, к чему приводит его использование. Начнем, как обычно, с реализации, показанной в листинге 13.29.
558 Часть IV. Ajax в примерах
Листинг 13.29. Метод start :ЯНННИНИННННННННН|
start: function() { this.applyButtonBehaviors();
new Effect.FadeTo( this.getLayer(1), 0.0, 1, 1, {} ); this.loadRSSFeed(0,true);
this.startSlideShow{false);
}, |
|
щ |
Метод applyButtonBehaviors настраивает обработчики событий onclick для кнопок перехода к предыдущей/последующей статье, паузы/возобновления, а также кнопки добавления новой ленты. Этот метод мы рассмотрим следующим. Эффект затенения, представленный во второй строке, вызывает постепенное исчезновение с экрана видимого элемента div, чтобы его можно было скрыть при загрузке первого слайда. Обратите внимание на то, что
вданной реализации мы не кодируем эффект самостоятельно, а используем готовое решение из библиотеки Rico, что уменьшает объем кода, который требуется писать, отлаживать и сопровождать. Метод loadRSSFeed инициализирует запрос Ajax на загрузку в первую ленту, а метод startSlideShow запускает таймер со значением slideTransitionDelay, инициируя таким образом слайд-шоу. Подробнее метод loadRSSFeed будет рассмотрен в разделе "'Загрузка RSS-лент с помощью Ajax", а метод startSlideShow — в разделе 'Реализация слайд-шоу". На этом мы, как и обещали, завершаем обсуждение построения и настройки, рассмотрев упомянутый в листинге 13.29 метод applyButtonBehaviors.
Как отмечалось ранее, метод applyButtonBehaviors подключает кнопки
кметодам, реализующим их поведение. Реализация этого метода показана
влистинге 13.30.
Листинг 13.30. Метод applyButtonBehaviors .
applyButtonBehaviors: function!) |
{ |
|
|
|
|
S(this.id + '__prevBtn') .onclick |
= |
this.previous.bind(this) ; |
|
|
$ ( t h i s . i d + ' _ n e x t B t n ' ) . o n c l i c k • t h i s . n e x t . b i n d ( t h i s ) ; |
|||
|
$ ( t h i s . i d + ' _ p a u s e B t n ' ) . o n c l i c k - t h i s . p a u s e . b i n d ( t h i s ) ; |
|||
|
$ ( t h i s . i d + ' _ a d d B t n ' ) - o n c l i c k = t h i s . a d d F e e d . b i n d ( t h i s ) ; |
|||
_ ± |
|
|
|
. |
Здесь, пожалуй, стоит напомнить, какой синтаксис и какие идиомы мы используем. В частности, мы задействовали пару синтаксических элементов библиотеки Prototype. Во-первых, это метод $, который можно рассматривать как вызов document .getElementByld. Во-вторых, метод bind неявно создает для нас замыкание, так что обработчик событий onclick для каждой кнопки может вызывать методы первого класса нашего компонента. Рассмотрим теперь детали реализации.
Реализация открывает неявный контракт между компонентом и HTMLразметкой приложения. Компонент создается с идентификатором, который хранится в атрибуте this . id . Позже этот идентификатор используется как префикс для нахождения различных элементов в разметке. В данном случае предполагается, что идентификаторы кнопок -— это метки, переданные в
Глава 13 Создание приложений Ajax, не использующих сервер 559
Таблица 13.4. Атрибуты контроллера
Атрибут |
Описание |
t h i s . currentFeed |
Экземпляр RSSFeed, в текущий момент загруженный в память |
t h i s . feedlndex |
Номер ленты, показываемой в текущий момент. Является |
|
индексом массива t h i s . o p t i o n s .rssFeeds |
t h i s . itemlndex |
Номер статьи, показываемой в текущий момент Является |
|
индексом массива статей объекта RSSFeed, показываемого |
|
в текущий момент |
конструктор, к которым добавлено _prevBtn, _nextBtn, _pauseBtn и addBtn. Чтобы проиллюстрировать сказанное, рассмотрим приведенный выше код. В качестве идентификатора здесь используется rssReader, а компонент ожидает, что кнопки будут заданы следующим образом:
<input type="button" id="rssReader_prevBtn" value=" « " /> <input type="button" id="rssReader_pauseBtn" value=" I I " /> <input type="button" id="rssReader_nextBtn" value=" » " /> <input type="button" id="rssReader_addBtn" value="Add Feed" />
Видно, что контроллер RSSReader начинает приобретать форму, и теперь мы можем рассмотреть детали реализации слайд-шоу.
Реализация слайд-шоу
Теперь самое время поговорить об изменении семантики по сравнению
спредыдущей версией сценария. Изначально мы загружали все RSS-ленты
впамять в момент запуска, а затем просто формировали переходы между внутренними представлениями лент. Это было просто, но масштабируемость такого решения вызывала определенные сомнения. Если мы регулярно прочитываем десятки или сотни RSS-лент, содержащих десятки статей, предварительная загрузка их всех была бы чересчур сильной нагрузкой на браузер. Поэтому в разделе реструктуризации мы попытаемся добиться нормальной масштабируемости и производительности приложения, так изменив семантику, чтобы за раз в память загружалась единственная RSS-лента. Все статьи RSSItems одной ленты побывают в памяти, однако в каждый отдельный момент времени в памяти будет присутствовать только один элемент RSSFeed. За то, на каком этапе находится слайд-шоу в процессе отображения содержимого, отвечают три атрибута контроллера, описанных в табл. 13.4.
Разобравшись с семантикой, переходим к навигации. Если мы планируем реализовать навигацию по всем статьям (элементам item) всех RSS-лент, то должны изучить несколько методов. Рассмотрим для начала пару методов перехода к предыдущей/следующей статье. Механизм перехода требуется не только для реализации явных событий, связанных с кнопками, но и для пассивного чтения при просмотре автоматизированного слайд-шоу.
Итак, рассмотрим пару булевых методов, сообщающих читателю, может ли он переходить вперед или назад. Реализация этих двух методов hasPrevious и hasNext показана в листинге 13.31.

560 Часть IV Ajax в примерах |
|
... ЛИСТИЙГ 13.31. Пара методов has Preyious/has Next |
: , |
hasPrevious: function() { |
|
return ! (this.feedlndex == 0 && this .iteinlndex == 0); |
|
}, |
|
hasNext: function() {
return !(this.feedlndex == this.options.rssFeeds.length - 1 && this.itemlndex == this.currentFeed.items.length - 1);
}, щ
С помощью представленных методов определяется, доступен ли предыдущий или последующий слайд. При указанной реализации предыдущий "слайд" будет доступен в том случае, если в текущий момент приложение демонстрирует не первую статью первой ленты, а следующие "слайд" доступен, если мы читаем не последнюю статью последней ленты.
Рассмотрим теперь, что такое переход на предыдущую и последующую статью ленты. Начнем с метода previous (), показанного в листинге 13.32.
I Листинг 13.32. Метод previous ()Г ^ 1 Д | Д
previous: function() {
if ( !this.hasPrevious() ) return;
var requiresLoad = this.itemlndex == 0;
this.fadeOut( this.visibleLayer, Prototype.emptyFunction ); this.visibleLayer = {this.visibleLayer + 1 ) % 2;
if { requiresLoad )
this.loadRSSFeedf this.feedlndex - 1, false ); else
setTimeout( this.previousPartTwo.bind(this), parselnt (this. options. fadeDuration/4) ). ;
Ь
previousPartTwo: function() |
{ |
|
|
this.itemlndex'—; this.updateView(); |
|
Ь |
|
m |
Первое, что мы сделали при написании метода previous (), — поместили в самом его начале защитное условие. Если предыдущей статьи не существует, метод previous () ничего не делает. Если значение requiresLoad равно true, тогда содержимое RSS-статьи, на которую планируется переход, еще не загружено. Если мы находимся на первой статье ленты и переходим назад, требуется загрузка предыдущей ленты. Метод затухания, подробно рассмотренный в разделе "Эффекты перехода", постепенно ослабляет видимый слой. Дальнейшие действия этого метода зависят от того, требуется ли перед отображением загрузка какого-либо содержимого. Если да, то мы инициируем загрузку необходимых данных посредством метода loadRSSFeed(). Первым параметром данного метода является номер загружаемой ленты, вторым — булево значение, указывающее направление: true — вперед, false (как в данном случае) — назад. Если же содержимое статьи уже загружено, то мы вызываем previousPartTwo() после паузы, равной одной четвертой общей длительности fadeDuration. Во "второй части" данного метода про-

Глава 13 Создание приложений Ajax, не использующих сервер 561
сто обновляется свойство itemlndex и вызывается функция updateView (), приводящая к затуханию соответствующего слайда.
Ну что, запутались? Ладно, объясняем нормальным языком: если текст, который нужно отобразить, не загружен, его загрузка начинается немедленно, что приводит к обновлению пользовательского интерфейса сразу же после поступления данных. Время, которое требуется на прием данных, используется для естественной реализации затухания! С другой стороны, если содержимое уже загружено {т.е. мы переходим на другую статью загруженной ленты), то мы вводим искусственную задержку (четверть времени затухания) перед проявлением следующей статьи. Довольно хитро, правда?
Метод next (), показанный в листинге 13.33, представляет собой реализацию алгоритма, обратного к приведенному выше.
next: |
function{} |
{ |
|
|
if |
( |
ithis.hasNext() |
) return; |
|
var |
requiresLoad = |
|
||
|
this.itemlndex == (this.currentFeed.items.length - 1); |
|||
|
this.fadeOut( this.visibleLayer, Prototype.emptyFunction ) ; |
|||
|
this.visibleLayer = (this.visibleLayer + 1 ) % 2; |
|||
if |
( |
requiresLoad > |
|
|
|
this.loadRSSFeed{ |
this.feedlndex + 1, true ); |
||
else |
|
|
||
|
setTimeout( |
this.nextPartTwo.bind(this), |
||
|
|
|
parselnt(this.options.fadeDuration/4) ); |
Ь
nextPartTwo: function() { |
|
this.itemlndex++; this.updateView(); |
|
Ь |
m |
Выглядит знакомо? Метод next () использует противоположную логику индексирования, а во всем остальном идентичен приведенному выше алгоритму. Обратите внимание на то, что при каждом переходе пара методов previous () /next {) переключает видимый слой с одного слайда на другой с помощью такого выражения:
this.visibleLayer = (this.visibleLayer + 1) % 2;
Таким образом, мы сообщаем коду, который в конечном счете обновит пользовательский интерфейс (после загрузки содержимого или явного вызова функции updateView ()), на какой слой помещать результат. Напомним, что контекстная область приложения содержит HTML-разметку, которые выглядит примерно следующим образом:
<!— Контекстная область —>
<div class="content" id="rssReader_content"> <div class="layerl">Layer 0</div>
<div class="layer2">Layer K/div> </div>
Здесь visibleLayer — целочисленное свойство, отслеживающее, в какой элемент div требуется помещать содержимое. Индекс 0 указывает, что при об-