Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Ajax в действии

.pdf
Скачиваний:
92
Добавлен:
01.05.2014
Размер:
6.34 Mб
Скачать

166 Часть 11. Основные подходы к разработке приложений

Рис. 4.6. Программа Mousemat отслеживает события, связанные с перемещениями курсора мыши в главной области, двумя способам: во-первых, координаты курсора мыши отображаются в строке состояния браузера, а во-вторых, в области малого размера перемещается точка, отражающая движения мыши

i d = ' t h u m b n a i l ' >

< d i v c l a s s = ' c u r s o r ' i d = ' c u r s o r ' / > < / d i v >

</ b o d y >

</ h t m l >

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

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

Глава 4. Web-страница в роли приложения 167

Обработчиком события является функция mouseObserver (). (Первая строка в теле функции необходима лишь из-за различий браузеров. В отличие оТ Mozilla, Opera и Safari, браузер Internet Explorer не передает параметры обработчику обратного вызова, а сохраняет объект Event в window.event.) В данном примере обработчик по очереди вызывает функции writestatus () й drawThumbnail (). Программа выполняет свое назначения, и, поскольку она предельно проста, код функции mouseObserver () понятен. В общем случае нам надо разработать другой способ объединения обработчиков, который был бы пригоден для разработки сложных программ.

Реализация образа разработки Observer в JavaScript-программе

Решением данной задачи может быть универсальный объект-маршрутизатор, который связывал бы с целевым элементом стандартную функцию, получающую управление при возникновении события, и управлял бы набором функций-обработчиков. Для этого придется переписать код инициализации следующим образом:

window.onload=function() {

var mat=document.getElementById('mousemat') ;

var mouseRouter=new jsEvent.EventRouter(mat,"onmousemove"); mouseRouter.addListener(writeStatus) ; mouseRouter.addListener(drawThumbnail);

}

Мы используем объект EventRouter и передаем ему в качестве параметра элемент DOM и тип события, которое хотим зарегистрировать. Затем мы включаем в объект-маршрутизатор функции-обработчики. Объектмаршрутизатор также поддерживает метод removeListener(), который мы здесь не используем. Применение нашего гипотетического объекта выглядит хорошо, но как мы реализуем сам объект?

Прежде всего нам надо написать конструктор объекта. В языке JavaScript это обычная функция. (В приложении Б содержится простое описание синтаксиса, используемого для создания объектов JavaScript. Если приведенный ниже код будет непонятным, обратитесь к этому описанию.)

jsEvent.EventRouter=function(el,eventType){ this.lsnrs=new Array();:

this.el=el; el.eventRouter=this ;

el[eventType]=j sEvent.EventRouter.callback;

}

Здесь мы определили массив функций-обработчиков; первоначально этот массив пуст. Затем мы получаем ссылку на элемент DOM и с помощью этой ссылки передаем элементу ссылку на текущий объект. Наши действия соответствуют образу разработки, описанному в разделе 3.5.1. Затем мы задаем в качестве обработчика события статический метод класса EventRouter с именем callback. Заметьте, что в JavaScript использование квадратных скобок а точки дают одинаковые результаты. Следовательно, два приведенных ниже выражения эквивалентны.

168 Часть II. Основные подходы к разработке приложений

el.onmouseover

el['onmouseover']

Этот факт мы используем, передавая имя свойства в качестве параметра. Данное решение можно сравнить с механизмом отражения в Java или .NET.

Рассмотрим функцию обратного вызова.

jsEvent.EventRouter.callback=function(event){ var e=event || window.event;

var router=this.eventRouter;

router.notify(e)

}

Контекстом для данной функции является не объект маршрутизации, а узел D O M . Мы извлекаем ссылку на EventRouter, которую присоединили к узлу D O M , используя прием, рассмотренный ранее. Затем вызываем метод notify () маршрутизатора, передавая ему в качестве параметра объект события.

Полностью код объекта-маршрутизатора показан в листинге 4.8.

Листинг4.8.ФайлEventRouter.js

var jsEvent=new Array();

jsEvent.EventRouter=function(el,eventType){ this.lsnrs=new Array();

this.el=el;

el.eventRouter=this;

el[eventType]=j sEvent.EventRouter.callback;

}

j sEvent.EventRouter.prototype.addListener=function(lsnr){ this.lsnrs.append(lsnr,true);

}

j sEvent.EventRouter.prototype.removeListener=function(lsnr){ this.lsnrs.remove(lsnr);

}

jsEvent.EventRouter.prototype.notify=function(e){ var lsnrs=this.lsnrs;

for(var i=O;i<lsnrs.length;i++){

var lsnr=lsnrs[i]; lsnr.call(this,e);

}

)

jsEvent.EventRouter.callback=function(event){ var e=event I| window.event;

var router=this.eventRouter; router.notify(e)

)

Заметьте, что некоторые из методов массива соответствуют не стандарту JavaScript, а расширенному определению массива, которое обсуждается в приложении Б. Функции addListener() и removeListener () легко реализовать, используя методы append () и remove (). Функции-обработчики вызываются с использованием метода Function, call (), первый параметр которого задает контекст функции, а последующие параметры (в данном случае — событие) передаются вызываемой функции.

Глава 4. Web-страница в роли приложения 169

Модифицированный вариант HTML-документа показан в листинге 4.9.

ЛИСТИНГ4.9. Документ mousemat.html, использующий объект EventRouter

<html> <head> <link rel='stylesheet' type='text/ess1 href='mousemat.ess' /> <script

type='text/javascript'

src='extras-array.js'X/script> <script

type='text/javascript'

src='eventRouter.js'X/script> <script

t ype='text/javascript'>

var cursor=null;

window.onload=function(){

var mat=document.getElementById('mousemat'); cursor=document.getElementById('cursor' ) ;

var mouseRouter=new jsEvent.EventRouter(mat,"onmousemove") ;

mouseRouter.addListener(writeStatus) ; mouseRouter.addListener(drawThumbnail);

}function writeStatus(e){ window.status=e.clientX+","+e.clientY

}function drawThumbnail(e){

cursor.style.left-((e.clientX/5)-2)+"px"; cursor.style.top=((e.clientY/5)-2)+"px";

} </script> </head> <body> <div class='mousemat' id='mousemat'x/div> <div class=' thumbnail' id='thumbnail'>

<div class='cursor1

id='cursor'/> </div> </body> </html>

Встроенный код JavaScript существенно упрощен. В данном случае нам надо лишь создать объект EventRouter, передать ему функции обработки и реализовать эти обработчики. Дальнейшим развитием документа и поддерживающего его кода может стать включение флажков опций, посредством которых пользователь сможет динамически добавлять или удалять обработчики. Мы предлагаем читателю самостоятельно реализовать данную возможность.

На этом мы заканчиваем обсуждение уровня контроллера в Ajaxприложении и роли, которую образы разработки, в частности Observer, играют в создании понятного и пригодного для сопровождения кода. В следующем разделе мы рассмотрим последний элемент архитектуры MVC — модель.

4.4. Модель в составе Ajax-приложения

Модель представляет собой область применения приложения — интерактивный магазин, музыкальный инструмент и даже набор точек в пространстве. Document Object Model, или DOM, не является моделью Ajaxприложения. В данном случае роль модели выполняет код, написанный на языке JavaScript. Подобно другим образам разработки, архитектура MVC неразрывно связана с объектным подходом.

170 Часть II. Основные подходы к разработке приложений

Разработчики JavaScript не задумывали его как объектно-ориентирован- ' ный язык, однако в нем, не затрачивая слишком много усилий, можно организовать некоторое подобие объектов. Посредством механизма прототипов можно определить нечто, похожее на классы, а некоторые разработчики пытаются даже реализовать наследование. Эти вопросы мы обсудим в приложении Б. До сих пор, создавая на JavaScript приложение, соответствующее архитектуре "модель-представление-контроллер", мы придерживались сти-Г ля программирования, типичного для JavaScript, например, передавали объекты Function непосредственно обработчикам событий. Определяя модель, желательно использовать объекты JavaScript и придерживаться объектного подхода настолько, насколько это имеет смысл для данного языка. В последующих разделах мы покажем, как это можно сделать.

4.4.1. Использование JavaScript для моделирования предметной области

Обсуждая представление, мы все время упоминали элементы DOM. Говоря о контроллере, мы были ограничены рамками модели событий, реализованной в браузере. При создании модели мы будем иметь дело почти исключительно с кодом JavaScript и практически не будем учитывать возможности браузеров. Для тех программистов, которые вынуждены постоянно бороться с несовместимостью браузеров, подобные условия работы покажутся предельно комфортными.

Рассмотрим простой пример. В главе 3 обсуждалось приложение для интерактивного магазина, а внимание в основном уделялось генерации данных серверными программами. Эти данные определяли товары в терминах идеи-; тификаторов, имен и описаний. Кроме того, учитывались цена, цвет и размер. Вернемся к данному примеру и рассмотрим, что произойдет, когда информация будет доставлена клиенту. В процессе работы приложение получает несколько подобных потоков данных и должно хранить их в памяти. Область памяти для хранения данных можно рассматривать как кэш на стороне клиента. Есть возможность быстро отобразить находящуюся в нем информацию, не обращаясь к серверу. О преимуществах такого подхода см. в главе 1.

Определим простой JavaScript-объект, соответствующий изделию, поддерживаемому сервером. Пример такого объекта приведен в листинге 4.10.

Листинг 4.10. Файл Garment, js

var garments=new Array(); function Garment(id,title,description,price){

t h i s . i d = i d ; garments[id]=this; this.title=title; this.description=description; this.price=price; this.colors=new Object(); this.sizes=new Object();

}

Garment.prototype.addColor(color){

Глава 4. Web-страница в роли приложения

171

t h i s . c o l o r s . a p p e n d ( c o l o r , t r u e ) ; } G a r m e n t . p r o t o t y p e . a d d S i z e ( s i z e ) {

t h i s . s i z e s . a p p e n d ( s i z e , t r u e ) ; )

Для хранения информации об изделиях определен глобальный массив. (Вы скажете, что использование глобальных переменных — плохой стиль программирования, и будете правы. При разработке реальных продуктов для этой цели обычно используются объекты пространств имен, однако в данном случае мы не будем поступать так, чтобы неоправданно не усложнять программу.) Это ассоциативный массив, для которого ключевыми значениями являются идентификаторы изделий. Такой подход гарантирует, что в каждый конкретный момент времени будет отображаться только одна ссылка на каждый тип товара. Все простые свойства, не являющиеся массивами, задаются в конструкторе. В момент определения массивы пусты. Для включения в них элементов используются специальные методы, которые обращаются к расширенному коду поддержки массивов (подробнее об этом — в приложении Б). Это также позволяет исключить дублирование данных.

Мы не создаем get- и set-методы и не реализуем полный контроль доступа посредством определения областей видимости для переменных и методов, что позволяют сделать объектно-ориентированные языки. Существуют способы обеспечения таких возможностей (они будут обсуждаться в приложении Б), но сейчас мы предпочитаем сделать модель как можно более простой.

При разборе потока XML-данных целесообразно сначала создать пустой объект Garment, а затем заполнять его информацией. Некоторые читатели удивятся, почему мы не предусмотрели более простой конструктор. На самом деле мы сделали это. Функции JavaScript позволяют задавать переменное количество параметров; недостающие параметры заменяются значениями null. Таким образом, два приведенных ниже вызова эквивалентны.

var garment=new Garment(123);

var garment=new Garment(123,null,null,null);

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

4.4.2. Взаимодействие с сервером

Для того чтобы генерировать объекты Garment в клиентской программе, нам надо организовать разбор XML-данных. О разборе мы уже говорили в главе 2, кроме того, мы вернемся к этому вопросу в главе 5, поэтому сейчас не будем детально обсуждать этот процесс. XML-документ содержит дескрипторы, атрибуты и содержимое элементов. Для чтения атрибутов мы используем свойство attributes и функцию getNamedItem(), а для чтения тела дескриптора — свойства f irstChild и data. Например:

garment.description=descrTag.firstChild.data;

С помощью этого выражения можно произвести разбор следующего XMLФрагмента:

172 Часть II. Основные подходы кразработке приложений

<description>Large tweedy hat looking like an unappealing strawberry </description>

Заметьте, что информация об изделиях автоматически помещается в глобальный массив по мере создания объектов; запись в массив предусмотрена в теле конструктора. Удалить изделие из массива также просто.

function unregisterGarment(id){ garments[id]«null;

}

Данный фрагмент кода удаляет из глобального массива, выполняющего функцию реестра, информацию об изделии, но не разрушает уже созданный экземпляр объекта Garment. Поэтому имеет смысл проверить, используется ли объект Garment.

Garment.prototype.isValid=function(){ return garments[this.id]!=null;

}

На текущий момент мы определили на стороне клиента простой способ обработки информации, передаваемой с сервера. На каждом шаге обработки используются объекты, простые для восприятия и поддержки. Повторим еще раз основные этапы разработки. Сначала мы генерируем на стороне сервера объектную модель. Исходной информацией являются данные из базы. В разделе 3.4.2 было рассмотрено решение этой задачи с помощью инструмента ORM (Object-Relational Mapping), который естественным образом обеспечивает двустороннее взаимодействие объектной модели и базы данных. Мы оформляем данные в виде объектов, модифицируем и сохраняем их.

Далее используем систему шаблонов для генерации потока XML-данных и выполняем разбор этого потока, чтобы создать объектную модель на уровне JavaScript-программы. На этом этапе мы выполняем разбор вручную. Возможно, вскоре появятся специализированные библиотеки, ориентированные на решение подобных задач.

Вприложении, предназначенном для администрирования, нам может понадобиться отредактировать данные, изменяя JavaScript-модель, а затем передать изменения серверу. При этом есть опасность появления двух копий модели, не синхронизированных друг с другом.

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

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

Глава 4. Web-страница в роли приложения 173

Модель на сервере нужна в любом случае, так как именно там находятся важные ресурсы, например, база данных, средства доступа к существующим программам и т.д. Таким образом, модель предметной области на стороне клиента обязана взаимодействовать с моделью на стороне сервера. Что же из этого следует? На этот вопрос мы попытаемся ответить в главе 5, где продоляшм разговор о взаимодействии клиента и сервера. Там же мы затронем вопросы разделения модели предметной области между различными уровнями.

На данный момент мы рассмотрели модель, представление и контроллер без учета их взаимодействия друг с другом. Закончим эту главу обсуждением вопросов совместной работы модели и представления.

4.5. Генерация представления на основе модели

формируя код, выполняемый на стороне клиента, в соответствии с архитектурой MVC, мы получили три отдельные подсистемы. Разделение функций позволяет создавать более понятные программы, но оно же приводит к увеличению объема кода. Противники применения образов разработки аргументируют свою позицию тем, что такой подход способен превратить решение простейшей задачи в сложнейшую процедуру (разработчикам, применяющим Enterprise JavaBeans, такая ситуация хорошо известна).

Создание приложений, насчитывающих несколько уровней, часто приводит к повторению информации на разных уровнях. Мы хорошо понимаем важность принципа DRY и знаем, что наилучший способ избежать дублирования — определять информацию на одном уровне и на ее основании генерировать остальные уровни. В данном разделе мы постараемся применить этот принцип на практике и опишем подход, позволяющий упростить реализацию архитектуры MVC. Обратим внимание на представление.

До сих пор мы рассматривали представление как код, созданный вручную и позволяющий отображать модель. Благодаря такому подходу мы могли достаточно свободно определять, что должен видеть пользователь. Однако в ряде случаев подобная свобода оказывается излишней, а программирование пользовательского интерфейса вручную всегда было скучным и утомительным занятием. Альтернативный подход предполагает автоматическую генерацию пользовательского интерфейса или хотя бы его компонентов на основе модели. Такой принцип уже использовался в среде Smalltalk и в системе Naked Objects Java/.NET. Язык JavaScript также вполне подходит для решения подобных задач. Рассмотрим, что можно сделать средствами отражения JavaScript, и создадим универсальный компонент "Object Browser", который может быть использован в качестве представления для любого объекта JavaScript.

4.5.1. Отражение объектов JavaScript

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

174 Часть II. Основные подходы к разработке приложений

Рис. 4.7. В данном примере ObjectViewer используется для отображения иерархической системы планет, для каждой из которых поддерживается ряд свойств. Кроме того, в массиве хранится дополнительная информация

код "вслепую", не имея предварительных сведений об объекте. Именно так приходится поступать при генерации интерфейса для объектов, составляющих модель. В идеале было бы неплохо реализовать универсальное решение, которое подходило бы для любой предметной области: финансовой деятельности, электронной коммерции, визуализации результатов научных исследований и т.д. В данном разделе вы ознакомитесь с JavaScript-библиотекой ObjectViewer, которую вы, может быть, захотите использовать при разработке своих приложений. Для того чтобы помочь вам составить представление об этой библиотеке, на рис. 4.7 показано несколько уровней сложного графа, отображаемых с помощью ObjectViewer.

Объект, выбранный для просмотра, представляет планету Меркурий. Этот объект достаточно сложен. Помимо обычных свойств, значениями которых являются числа или строки, он содержит URL изображения и массив с дополнительными данными. ObjectViewer обрабатывает всю информацию, не имея предварительных сведений о типе объекта.

Процесс исследования объекта, выяснения его свойств и возможностей называется отражением (reflection). Читатели, имеющие опыт работы с Java или .NET, уже знакомы с этим термином. Возможности отражения, реализованные в языке JavaScript, мы рассмотрим подробно в приложении Б. Здесь же только скажем, что объект JavaScript можно исследовать так, как будто он представляет собой ассоциативный массив. Приведенный ниже фрагмент кода выводит информацию о свойствах объекта.

var description»""; for (var i in MyObj){

var property=MyObj[i];

description+=i+" = "+property+"(BBSS)n"; } alert(description);

Представление данных посредством окна, предназначенного для вывода сообщений, — не самое удачное решение, так как это окно может не сочетаться с остальными компонентами интерфейса. Мы выбрали такой подход лишь для того, чтобы упростить пример. В листинге 4.11 представлен код ObjectViewer.

Глава 4. Web-страница в роли приложения 175

Листинг4.11.БиблиотекаObjectViewer

objviewer.ObjectViewer=function(obj,div,isInline,addNew){

styling.removeAllChildren(div); this.obj ect=obj; this.mainDiv=div; this.mainDiv.viewer=this ; this.islnline=islnline; this.addNew=addNew;

var table=document.createElement("table"); this.tbod=document.createElement("tbody"); table.appendChild(this.tbod); this.fields=new ArrayO;

this.children=new ArrayO; for (var i in this.object){

this.fields[i]=new objviewer.PropertyViewer( this, i

);

}

objviewer.PropertyViewer=function(obj ectViewer,name){ this.objectViewer=objectViewer;

this.name=name; this.value=objectViewer.object[this.name] ; this.rowTr=document.createElement("tr") ;

this.rowTr.className='objViewRow' ; this.valTd=document.createElement("td");

this.valTd.className='objViewValue' ; this.valTd.viewer=this; this.rowTr.appendChild(this.valTd) ; var valDiv=this.renderSimple(); this.valTd.appendChild(valDiv); viewer.tbod.appendChild(this.rowTr);

}

objviewer.PropertyViewer.prototype.renderSimple=function() { var valDiv=document.createElement("div");

var valTxt=document.createTextNode(this.value); valDiv.appendChild(valTxt);

if (this.spec.editable){ valDiv.className+=" editable"; valDiv.viewer=this;

valDiv.onclick=objviewer.PropertyViewer.editSimpleProperty;

}

return valDiv;

}

Всоставбиблиотекивходятдваобъекта:ObjectViewer,предназначенный Для просмотра объекта и формирования HTML-таблицы, и PropertyViewer, который отображает имя и значение отдельного свойства и представляет их

ввиде строки таблицы.

Вцелом задача решена, но существует ряд проблем. Во-первых, ОЬ- j ectViewer исследует каждое свойство. Если вы добавите к прототипу вспомогательные функции, Obj ectViewer обнаружит их. Если вы обработаете таким способом узел D O M , то увидите все встроенные свойства и сможете оценить, насколько сложен элемент D O M . В большинстве случаев нет необхо-