Роль сервера в работе Ajax-приложения

Глава из книги “Ajax в действии”

Авторы: Дейв Крейн
Эрик Паскарелло
Даррен Джеймс

Источник: Ajax в действии
Материал предоставил: Издательство ''Вильямс''
Опубликовано: 01.07.2006
Версия текста: 1.0
5.1 Программы, выполняемые на стороне сервера
5.2 Создание программ на стороне сервера
5.2.1 Популярные языки программирования
5.2.2 N-связные архитектуры
5.2.3 Управление моделью предметной области на стороне клиента и на стороне сервера
5.3 Принципы создания программ на стороне сервера
5.3.1 Серверные программы, не соответствующие основным принципам разработки
5.3.2 Использование архитектуры Model2
5.3.3 Использование архитектуры на базе компонентов
5.3.4 Архитектуры, ориентированные на использование Web-служб
5.4 Частные решения: обмен данными
5.4.1 Взаимодействие, затрагивающее только клиентскую программу
5.4.2 Пример отображения информации о планетах
5.4.3 Взаимодействие, ориентированное на содержимое
5.4.4 Взаимодействие, ориентированное на сценарий
5.4.5 Взаимодействие, ориентированное на данные
5.5 Передача данных серверу
5.5.1 Использование HTML-форм
5.5.2 Использование объекта XMLHttpRequest
5.5.3 Управление обновлением модели
5.6 Резюме
5.7 Ресурсы

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

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

Помимо рассмотрения общих вопросов, связанных с работой приложения, мы также уделим внимание деталям взаимодействия между клиентом и сервером. В главе 2 вы получили основные сведения об объекте XMLHttpRequest и скрытых фреймах. В данной главе мы вернемся к этому вопросу и обсудим использование различных образов разработки для передачи данных клиенту и выясним, есть ли альтернатива разбору XML-документов с использованием методов DOM. В последнем разделе мы представим систему управления трафиком, связанным с обменом между клиентом и сервером в процессе работы приложения. На стороне клиента будет находиться очередь запросов, а на стороне сервера — процессы для их обработки.

Начнем разговор с рассмотрения роли сервера в Ajax-приложениии.

5.1 Программы, выполняемые на стороне сервера

В процессе работы Ajax-приложения сервер выполняет две основные функции, которые существенно отличаются одна от другой. Во-первых, он доставляет приложение браузеру. Мы считаем, что в процессе доставки данные не претерпевают изменению, поэтому реализуем средства, предназначенные для выполнения на стороне клиента, как набор файлов .html, .css и .js. С передачей их клиенту справится даже самый простой сервер. Такое решение вполне жизнеспособно, но оно не единственное. Существуют альтернативные варианты. Средства для создания серверных программ мы подробно обсудим в разделе 5.3.

Вторая задача сервера — это взаимодействие с клиентом: обработка запросов и подготовка ответов. Поскольку HTTP — единственный возможный в данной ситуации транспортный механизм, взаимодействие всегда должно начинаться по инициативе клиента. Сервер может только отвечать. В главе 4 мы говорили о том, что Ajax-приложение должно поддерживать модель предметной области как на стороне клиента (для обеспечения быстрого отклика), так и на стороне сервера (для доступа к ресурсам, например, к базе данных). Синхронизация этих моделей — сложная задача и клиент без помощи сервера не может справиться с ней. Вопросы работы сервера будут рассмотрены в разделе 5.5; там же будут предложены решения данной проблемы, основанные на использовании одного из образов разработки, описанных в главе 3.

Как вы узнаете, прочитав данную главу, доставить приложение клиенту и организовать взаимодействие с ним можно различными способами. Существует ли наилучшее решение? Может ли определенное сочетание средств обеспечить их взаимную поддержку? Как различные решения будут работать с уже имеющимся программным обеспечением? Чтобы ответить на эти вопросы, необходимо описать возможности, имеющиеся в нашем распоряжении. Это мы постараемся сделать в данной главе. Рассмотрим варианты участия сервера в работе Web-приложения и особенности, связанные с применением инфраструктуры Ajax.

5.2 Создание программ на стороне сервера

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

5.2.1 Популярные языки программирования

Для создания программного обеспечения, предназначенного для выполнения на стороне сервера, могут быть использованы различные языки. Несмотря на то, что вся история Internet очень коротка, отношение разработчиков к тем или иным языкам программирования не раз изменялись. В настоящее время чаще всего используются PHP, Java и классическая технология ASP; также становятся все более популярными ASP.NET и Ruby. Эти названия, конечно же знакомы большинству читателей. Ajax имеет дело преимущественно с клиентскими программным обеспечением, которое может взаимодействовать с серверными программами, написанными на любом из этих языков. Более того, при использовании Ajax становится не столь важно, какой именно язык применялся при создании серверных программ. В результате упрощается перенос Ajax-приложений с одной платформы на другую.

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

5.2.2 N-связные архитектуры

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

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

Первые системы Weв включали браузер, взаимодействующий с Web-сервером. Сервер обычно отвечал за передачу браузеру файлов, находящихся в файловой системе серверной машины.

По мере развития и усложнения Web-прилжоений потребовался доступ к базам данных. Двухуровневая модель клиент-сервер уступила место трехуровневой модели, в которой сам Web-сервер занял место промежуточного уровня между клиентом и СУБД. Дальнейшее развитие привело к выделению на промежуточном уровне средств представления и бизнес логики, Они реализовывались либо в виде отдельных процессов либо как модули в составе одной программы.

Современное Web-приложение обычно имеет два основных уровня. Уровень бизнес-логики моделирует предметную область и непосредственно взаимодействует с базой данных. Уровень представления получает данные от средств бизнес-логики и представляет их пользователю. Браузер занимает в этой системе место низкоуровневого клиента.

С появлением Ajax стал развиваться клиентский уровень. Если ранее средства представления, полностью отвечали за поддержку работы пользователя, то сейчас их функции разделились между сервером и клиентом (рис. 5.1). Роль уровня представления на стороне сервера стала менее важной, поскольку некоторые вопросы обеспечения последовательности действий при работе пользователя полностью решаются на уровне клиента. Соответствующие программные средства создаются на языке JavaScript и работают в среде браузера.


Рис. 5.1. В Ajax-приложении некоторые функции уровня представления переместились со стороны сервера на сторону клиента. В результате возник новый уровень — клиентский уровень представления

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

5.2.3 Управление моделью предметной области на стороне клиента и на стороне сервера

В приложении Ajax нам по-прежнему необходимо моделировать предметную область на сервере, “в непосредственной близости” от базы данных и других жизненно важных ресурсов. Однако для того, чтобы клиентская программа могла обеспечить быстрый отклик на действия пользователя, она должна обладать определенным “интеллектом”, а это требует наличия на стороне клиента хотя бы частично реализованной модели. В результате возникает проблема синхронизации этих двух моделей.

Добавление дополнительного уровня всегда связано с увеличением сложности и возрастанию накладных расходов при взаимодействии. К счастью, данная проблема не нова. Аналогичные трудности возникали при разработке Web-приложений средствами J2EE, где были строго разделены уровень бизнес-логики и уровень представления. Модель предметной области поддерживается средствами бизнес-лоники. К ней обращается уровень представления, на котором затем генерируется содержимое Web-страниц, передаваемых браузеру. В J2EE проблема была решена путем применения “объектов передачи” (transfer objects), которые представляют собой обычные объекты, передаваемые между уровнями. Они представляют уровню представления ограниченный доступ к модели предметной области.

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

При организации взаимодействия между уровням приходится решать две основных задачи: чтение данных с сервера и запись их на сервер. Подробно они будут рассматриваться в разделах 5.3 — 5.5. Перед тем как завершить разговор об архитектуре, мы должны рассмотреть основные принципы создания серверных программ, используемых в настоящее время. В частности нас интересуют вопросы формирования модели предметной области, организации доступа к ней с уровня представления и ограничения, которые могут возникнуть при использовании Ajax.

Согласно данным исследований (ссылка на соответствующий обзор приведена в конце данной главы), в настоящее время только для Java существует более 60 вариантов базовых средств разработки (по правде говоря, такое обилие предложений скорее мешает, чем помогает в работе). Большинство из них различается лишь в деталях и, независимо от языка реализации серверных программ, мы можем выделить для уровня представления несколько основных архитектурных принципов. Рассмотрим их.

5.3 Принципы создания программ на стороне сервера

Базовые средства, применяемые для разработки программ на стороне сервера, важны для Ajax-приложений. Если код клиента генерируется на базе модели, поддерживаемой на сервере, их значение еще более возрастает. Если мы вручную создадим код клиента и оформим его в виде статических HTML-документов, содержащих JavaScript-код, то базовые средства не будут участвовать в доставке приложения, но данные, требуемые приложению в процессе работы по-прежнему должны генерироваться динамически. Как было сказано ранее, программы на стороне сервера обычно разделяются на два уровня: модель предметной области и представление. В случае Ajax-приложения средства представления выполняют роль посредника между моделью и клиентской частью. Для того, чтобы приложение работало эффективно, надо правильно организовать взаимодействие клиента с сервером, а для этого необходимо знать архитектуру серверных программ и, возможно, инструменты, применявшиеся при их создании.

Некоторые всерьез считают сервер в составе Web-приложения игрушкой в руках разработчика. Проблема поддержки работы пользователя посредством набора Web-страниц и обращения к другим системам на сервере, например СУБД, еще никогда не была по-настоящему решена. Web буквально наводнена неудачными системами и утилитами, а новые проекты появляются каждый месяц, а то и каждую неделю.

К счастью, среди этих разнообразных проектов можно выделить группы со сходными характеристиками. Анализируя основные подходы можно описать четыре основных способа решения задач, поставленных перед сервером. Рассмотрим их и выясним, подходят ли они для нашей Ajax-модели.

5.3.1 Серверные программы, не соответствующие основным принципам разработки

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


Рис. 5.2. Web-программа, созданная без учета основных принципов разработки. Каждая страница, сервлет или CGI-сценарий поддерживает собственную логику и особенности представления данных. Вспомогательные методы и объекты могут инкапсулировать низкоуровневые функции общего назначения, например, обращение к базе данных

Применить данный подход для Ajax-приложения достаточно просто, при условии, что код клиента создается вручную. Генерация клиентской программы сервером — сложная задача и ее рассмотрение выходит за рамки данной книги. Для доставки клиента надо определить основную страницу, содержащую JavaScript-код, таблицы стилей и другие ресурсы. Для доставки данных нам надо лишь заменить HTML-документы, генерируемые сервером, на XML-данные или информацию в другом формате.

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

В Ajax-приложении проблема, возможно, будет не столь острой, поскольку гипертекстовые ссылки в этом случае встречаются несколько реже, однако включение инструкций и перенаправление их между страницами все равно усложнит разработку. В простых XML-документах включение и перенаправление не требуется, но в приложениях большого объема может возникать необходимость в пересылке документов со сложной структурой, сформированных в результате совместной работы нескольких процессов. В ранних разработках для преодоления данной проблемы использовалась архитектура MVC. Приложения, созданные по такому принципу часто встречаются в настоящее время.

5.3.2 Использование архитектуры Model2

Образ разработки Model2 является разновидностью MVC. Отличается он тем, что контроллер имеет единственную точку входа и единственное определение последовательности действий пользователя. В применении к Web-приложению, это означает, что одна страница контроллера или один сервлет отвечает за маршрутизацию большинства запросов, передавая их различным службам и пересылая полученные данные представлению. Среди базовых наборов средств, поддерживающих Model2, наиболее известным является Apache Struts. Данной архитектуре также соответствует ряд других инструментов для Java и PHP. На рис. 5.3 условно показано Web-приложение, соответствующее архитектуре Model2


Рис. 5.3. Архитектура Model2. Единственная страница контроллера или единственный сервлет получает все запросы и распределяет их в соответствии со схемой работы пользователя. Запрос может быть передан для обработки вспомогательным классам или функциям и, перед пересылкой браузеру, передается компоненту, выполняющему роль представления (например JSP- или PHP-документу)

Как же применить данный принцип разработки к серверному приложению, взаимодействующему с клиентом Ajax? Model2 практически никак не определяет доставку клиентского приложения, потому что обычно она производится в начале работы и выполняется практически одинаково для всех пользователей. Централизованный контроллер может участвовать в процессе аутентификации, однако этот факт не влияет на доставку.

Данная архитектура хорошо подходит для доставки данных. Представление, созданное в рамках архитектуры Model2, практически не зависит от базовых средств и мы можем без труда заменить HTML на XML или другой формат. Часть функций контроллера передается уровню клиента, но другие функции реализуются на стороне сервера посредством отображения.

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

5.3.3 Использование архитектуры на базе компонентов

При создании HTML-страницы для классического Web-приложения в распоряжении автора имеется ограниченный набор компонентов пользовательского интерфейса; обычно этот набор исчерпывается элементами HTML-формы. Их возможности остаются неизменными уже около 10 лет и они существенно проигрывают современным инструментам, предназначенным для создания интерфейсов. Если автор захочет реализовать нечто вроде древовидной структуры или таблицы с редактируемыми ячейками, он вынужден будет заняться низкоуровневым программированием. Это не идет ни в какое сравнение с уровнем абстракции, доступным разработчику, создающему программы для настольной системы и имеющему в своем распоряжении такие инструменты как MFC, GTK+, Cocoa, Swing или Qt.

Компоненты для Web

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


Рис. 5.4. Архитектура, основанная на использовании компонентов. Приложение описывается как набор компонентов, для воспроизведения которых браузеру передается поток HTML-данных и JavaScript-программ. Каждый компонент содержит собственные “микро-реализации” модели, представления и контроллера. Контроллер более высокого уровня обрабатывает запросы браузера к отдельным компонентам и модели предметной области

Многие архитектуры на базе компонентов описывают взаимодействие с пользователем в терминах, применяемых при работе на настольных системах. Так, например, с компонентом Button может быть связан обработчик события, соответствующего щелчку мыши, поле редактирования может иметь обработчик valueChange и т.д. В большинстве архитектур обработка событий делегируется серверу посредством запроса, который формируется в ответ на действие пользователя. В “интеллектуальных” архитектурах обработка событий осуществляется незаметно для пользователя, а в других при возникновении каждого события обновляется вся страница. Как правило, в приложениях, созданных в виде набора компонентов, взаимодействие с сервером осуществляется более эффективно, по сравнению, например, с приложениями Model2.

Одна из целей, преследуемых архитектурой, о которой идет речь — это формирование различных типов пользовательского интерфейса на базе единого описания. Некоторые наборы базовых средств, например, Windows Forms для .NET или JSF (JavaServer Faces) обеспечивают такую возможность.

Применимость архитектуры на базе компонентов для Ajax-приложений

Как же сочетается архитектура на базе компонентов с Ajax? На первый взгляд, взаимодействие должно быть органичным, поскольку оба подхода предполагают переход от интерфейса на основе документов к использованию компонентов. Может показаться, что данную архитектуру целесообразно применять для Ajax-приложений, в особенности тогда, когда клиентские средства генерируются программой и существуют включаемые средства воспроизведения, поддерживающие Ajax. Такой подход выглядит весьма привлекательным, поскольку в этом случае исчезает необходимость специального изучения разработчиками особенностей языка JavaScript и существует возможность реализовать систему воспроизведения на базе обычного HTML-кода.

Подобное решение хорошо подходит для тех приложений, для которых требуются лишь стандартные типы компонентов. Тем не менее, некоторая степень гибкости все же теряется. Успех Google Maps (см. главу 1) в значительной степени обусловлен тем, что для данной системы определен собственный набор компонентов, от прокручиваемой карты до элементов масштабирования и всплывающей подсказки. Реализовать ту же систему, используя стандартный набор компонентов, типичных для настольной системы, было бы гораздо сложнее и конечный результат наверняка получился бы гораздо худшим.

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

Для того, чтобы архитектура была пригодной для Ajax-приложения, она должна обеспечивать эффективную передачу данных в процессе работы. Здесь проблемы могут оказаться более серьезными, так как контроллер жестко “привязан” к уровню сервера и определен с помощью выразительных средств, типичных для настольных систем. Для Ajax-приложения, способного обеспечивать требуемый отклик на действия пользователя, необходимо иметь возможность определять собственные обработчики событий, которую сервер не всегда может предоставить. Однако данная архитектура имеет серьезные потенциальные возможности и по мере возрастания популярности Ajax несомненно будут появляться новые решения. Система CommandQueue, которую мы рассмотрим в разделе 5.5.3 может стать существенным шагом по пути применения JSF и других подобных технологий, однако на сегодняшний день она еще не готова. Имеющиеся же в наличии базовые средства не предоставляют клиентам той свободы, которую хотелось бы видеть.

Будущее покажет, насколько хорошо удастся адаптировать системы на базе компонентов для Ajax. В настоящее время наблюдается рост интереса со стороны корпорации Sun и некоторых поставщиков JSF к инструментам, созданным с учетом Ajax. Поддержка некоторых функций, применимых для Ajax, уже реализована в .NET Forms; они также будут доступны в наборе инструментальных средств Atlas.(Ссылки на эти системы приведены в конце главы.)

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

5.3.4 Архитектуры, ориентированные на использование Web-служб

Последняя архитектура из рассматриваемых в данной главе, — это SOA (service-oriented architecture), т.е. архитектура, основанная на использовании служб. В данном случае служба — это нечто, к чему можно обратиться по сети, и получить в качестве ответа структурированный документ. Основное внимание здесь уделяется не содержимому, а данным, что вполне соответствует принципам Ajax. В настоящее время наиболее часто используются Web-службы, а XML в качестве основного языка также с энтузиазмом воспринимается разработчиками Ajax-приложений.

Термин Web-служба можно понимать как в широком, так и в узком смысле. Web-служба в узком смысле — это средства, использующие протокол SOAP. Web-служба в широком смысле — это любая система обмена данными, базирующаяся на протоколе HTTP, независимо от того, применяется ли при ее работе протокол SOAP или формат XML. XML-RPC, JSON-RPC и любая система, которую вы разработаете, используя объект XMLHttpRequest, будет Web-службой в широком смысле.

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

Предоставление доступа к объектам на стороне сервера

Многие инструментальные средства позволяют представить обычный объект, созданный на Java, C# или PHP, как Web-службу. При этом поддерживается отображения методов объекта в интерфейс Web-службы. Такую возможность предоставляют, в частности, Microsoft Visual Studio и Apache Axis for Java. Многие инструменты Ajax, например, DWR (для Java) и SAJAX (для PHP, .NET, Python и некоторых других языков) обеспечивают данную возможность посредством клиентского кода, написанного на языке JavaScript.

Применение инструментальных средств может существенно помочь при создании приложения. Если же использовать их неумело, результат будет противоположным. Для того, чтобы составить представление о правильном использовании инструментов, рассмотрим простой пример, в котором применяется Java DWR. Мы определим объект на стороне сервера для представления персональной информации.

package com.manning.ajaxinaction;

public class Person{
  private String name=null;
  public Person(){
  }
  public String getName(){
    return name;
  }
  public void setName(String name){ 
    this.name=name; 
  }
}

Данный объект должен соответствовать спецификации JavaBeans. Это означает, что он должен предоставлять конструктор, вызываемый без параметров и обеспечивать доступ к требуемым полям для чтения и записи посредством get- и set-методов. Далее мы сообщаем DWR о том, что данный объект должен быть доступен уровню JavaScript. Для этого мы редактируем файл dwr.xml.

<dwr>
  <init>
    <convert id="person" converter="bean" 
      match="com.manning.ajaxinaction.Person"/>
  </init>
  <allow>
    <create creator="new" javascript="person"> 
      <param name="class" value="com.manning.ajaxinaction.Person">
    </create>
  </allow>
</dwr>

В разделе init мы определяем средства преобразования нашего класса в тип bean, а в разделе allow определяем средства, представляющие данный объект JavaScript-программам в виде переменной person. Наш объект Person содержит только один общедоступный метод, getName(), таким образом мы можем включить в код клиента Ajax следующее выражение.

var name=person.getName();

Затем можно получать требуемое значение с сервера в асинхронном режиме.

В нашем классе Person содержится только один метод. На первый взгляд может показаться, что им и исчерпывается набор средств, которые предоставляются клиенту. На самом деле это не так. Класс Person является подклассом java.lang.Object и наследует от него ряд общедоступных методов, например, hashCode() и toString(). К ним также может производиться обращение. Эти “скрытые” средства не являются специфическими для DWR. Метод JSONRPCBridge.registerObject(), например, делает тоже самое. Следует заметить, что DWR позволяет ограничить доступ к определенным методам, настраивая конфигурационный XML-файл. Однако по умолчанию доступ предоставляется ко всем методам. Такая проблема типична для решений, основанных на отражении. Мы встретились с ней в главе 4 при создании первых вариантов ObjectViewer. Рассмотрим способы ее решения.

Предоставление ограниченного доступа

Если мы ненамеренно дадим возможность пользователям Web вычислять хэш-коды наших объектов, может ли это представлять опасность для нас? Для рассматриваемого примера, по-видимому, нет, так как суперклассом создаваемого нами класса является java.lang.Object, и эта ситуация вряд ли изменится. Для более сложных моделей, предоставляя информацию о суперклассе, мы не гарантируем, что программа в дальнейшем не подвергнется реструктуризации. Вполне может получиться так, что разработчик клиентской программы захочет воспользоваться методами, доступ к которым вы непреднамеренно предоставили. Затем, когда реструктуризированная модель будет доставлена на сервер, программные средства на стороне клиента внезапно откажутся работать. Другими словами, такое решение препятствует корректному разделению функций между клиентом и сервером. Если вы используете инструментальные средства, подобные DWR или JSON-RPC, вам следует тщательно взвешивать решения относительно состава интерфейса Ajax. Возможно, вам даже придется создать нечто вроде объекта Facade (рис. 5.5).


Рис. 5.5. Одна из систем предоставляет Ajax-клиенту все объекты как Internet-службы, а при создании другой использован образ разработки Facade, который гарантирует доступ лишь к ограниченному набору функций. Уменьшая число общедоступных методов мы снижаем риск повредить клиентский код при реструктуризации серверных программ

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

Еще одно преимущество Facade состоит в том, что данный образ разработки позволяет определять степень детализации предоставляемых услуг. Хорошая модель предметной области обычно содержит большое количество методов, выполняющих несложные действия. Таким образом удовлетворяется требование детального управления работой кода на стороне сервера. Ajax-клиент предъявляет к интерфейсу Web-службы совершенно другие требования; они вытекают из наличия задержки, связанной с передачей данных по сети. Большое количество методов, решающих частные задачи, резко снизят практичность клиента, а сервер может не справиться с большим количеством запросов, передаваемых по сети.

Различие требований к интерфейсу, предъявляемым клиентом и сервером можно проиллюстрировать на примере. Сравним разговор с обменом письмами. (Говоря о письмах мы имеем в виду не электронную почту, а именно письма, написанные на бумаге, доставка которых занимает несколько дней.) Когда два человека беседуют, как минимум несколько фраз. которыми они обмениваются друг с другом, посвящена стандартной теме “как дела?”. При написании письма никто не позволяет себе задать один подобный вопрос и ждать ответа на него. Отправитель подробно описывает, как его здоровье, как он провел отпуск, какие новости он узнал недавно — все это оформляется в виде одного документа.

Путем объединения отдельных запросов, предназначенных для передачи по сети, в один большой документ, архитектура, ориентированная на использование сетевых служб позволяет более эффективно применять доступные сетевые ресурсы. Как правило проблема быстродействия линий связи решается проще, чем проблема задержки при передаче по сети. Существует также проблема выработки стандартов для передачи больших объемов данных в формате XML посредством знакомого всем протокола HTTP, но ее мы рассматривать не будем. Если мы проанализируем возможности, предоставляемые Ajax, то увидим, что в нашем распоряжении есть встроенные в браузер средства поддержки HTTP и XML, поэтому имеет смысл создавать распределенную модель предметной области на базе документов.

Типичный документ, например, данная книга, состоит из абзацев, заголовков, таблиц и рисунков. Аналогично, документ, содержащийся в в обращении к службе, может содержать различные элементы, например, запросы, данные для обновления и оповещения. Образ разработки Command, который обсуждался в главе 3, позволяет представлять структуру документа в виде последовательности действий (которые при необходимости можно отменять), передаваемых между клиентом и сервером. Реализация подобной структуры будет рассмотрена далее в этой главе.

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

Предположим, что мы приняли решение в пользу той или иной архитектуры и начали разработку Ajax-приложения. В главе 4 мы уже обсуждали архитектуру клиентской части приложения, а пример получения с сервера данных в формате XML был рассмотрен в главе 2. XML — удобный и популярный, но не единственный возможный формат для обмена данными между клиентом и сервером. В следующем разделе мы обсудим все возможности организации подобного взаимодействия.

5.4 Частные решения: обмен данными

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

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

5.4.1 Взаимодействие, затрагивающее только клиентскую программу

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

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

5.4.2 Пример отображения информации о планетах

Перед тем как заняться изучением различных механизмов обмена данными, давайте представим себе простой пример, на котором будем проверять то или иное решение. Наше приложение будет представлять информацию о планетах солнечной системы. В главном окне представлено изображение солнечной системы с пиктограммой для каждой планеты. На сервере хранятся сведения о планетах; по щелчку на пиктограмме, соответствующей планете, надо отобразить эти сведения в окне (рис. 5.6). Сейчас мы не будем использовать объект ObjectViewer, который был рассмотрен в главе 4, но вернемся к нему далее в этой главе.


Рис. 5.6. По щелчку на пиктограмме, соответствующей планете, информация о ней выводится в окне

В данном случае нас больше всего интересует процесс доставки данных, которые должны быть отображены. Мы обсудим форматы данных, передаваемых сервером, но не будем углубляться в детали их генерации так как этот вопрос был рассмотрен в главе 3. В листинге 5.1 показана заготовка клиентского кода приложения. На ее основе мы будем исследовать различные механизмы доставки данных.

Листинг 5.1. Содержимое файла popups.html
<!DOCTYPE html PUBLIC 
"-//W3C//DTD XHTML 1.0 Strict//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Planet Browser</title>
<link rel=stylesheet type="text/css" 
  href="main.css"/>
<link rel=stylesheet type="text/css" 
  href="windows.css"/>
<link rel=stylesheet type="text/css" 
  href="planets.css"/>

// (111) Включение библиотек JavaScript
<script type="text/javascript" 
  src="x/x_core.js"></script> 
<script type="text/javascript" 
  src="x/x_event.js"></script>   
<script type="text/javascript" 
  src="x/x_drag.js"></script>
<script type="text/javascript" 
  src="windows.js"></script> 
<script type="text/javascript" 
  src="net.js"></script>    
<script type="text/javascript">

window.onload=function(){
  var pbar=document.getElementById("planets");
  var children=pbar.getElementsByTagName("div");
  for(var i=0;i<children.length;i++){
    // (222) Связывание с пиктограммами обработчиков событий
    children[i].onclick=showInfo;   
  }
}

</script>

</head>
<body>

// (333) Включение пиктограмм, соответствующих планетам
<div class="planetbar" id="planets">   
<div class="planetbutton" id="mercury">
 <img src="img/ball-mercury.gif" alt="mercury"/>
</div>
<div class="planetbutton" id="venus">
 <img src="img/ball-venus.gif" alt="venus"/>
</div>
<div class="planetbutton" id="earth">
 <img src="img/ball-earth.gif" alt="earth"/>
</div>
<div class="planetbutton" id="mars">
 <img src="img/ball-mars.gif" alt="mars"/>
</div>
<div class="planetbutton" id="jupiter">
 <img src="img/ball-jupiter.gif" alt="jupiter"/>
</div>
<div class="planetbutton" id="saturn">
 <img src="img/ball-saturn.gif" alt="saturn"/>
</div>
<div class="planetbutton" id="uranus">
 <img src="img/ball-uranus.gif" alt="uranus"/>
</div>
<div class="planetbutton" id="neptune">
 <img src="img/ball-neptune.gif" alt="neptune"/>
</div>
<div class="planetbutton" id="pluto">
 <img src="img/ball-pluto.gif" alt="pluto"/>
</div>
</div>

</body>
</html>

В состав нашего файла мы включили несколько JavaScript-библиотек (111). Средства, предоставляемые net.js, обеспечивают поддержку низкоуровневых HTTP-запросов, используя для этой цели объект XMLHttpRequest, который обсуждался в главе 2. В файле windows.js определен объект окна, допускающего перетаскивание. Это окно мы используем для отображения информации. Детали реализации окна в данном случае не имеют значениями; нас интересует только порядок вызова конструктора.

var MyWindow=new Window(bodyDiv,title,x,y,w,h);

Здесь bodyDiv — это элемент DOM, который выводится в окне, title — строка, отображаемая в заголовке окна, а параметры x, y, w и h определяют начальные размеры окна. Задавая элемент DOM в качестве параметра, мы обеспечиваем достаточную гибкость при отображении данных к окне. Полностью код объекта Window можно скопировать с Web-узла, посвященного данной книге. Посредством HTML-кода, содержащегося в файле, мы определяет элемент div для каждой планеты (333), а в функции window.onload (222) мы связываем с пиктограммами планет обработчики onclick. В качестве обработчиков используются функции showInfo(), которые в данном листинге не определены. В этой главе мы обсудим несколько вариантов их реализации. Рассмотрим действия, которые мы можем предпринять тогда, когда нам требуется загрузка данных.

5.4.3 Взаимодействие, ориентированное на содержимое

Наши первые шаги по пути использования подхода Ajax напоминают действия, которые предпринимаются при создании классического Web-приложения. В этом нет ничего удивительного; как было сказано в главе 1, в названии первых велосипедов присутствовало слово “лошадь”. Взаимодействие, ориентированное на содержимое, соответствует классическому подходу, но может иметь место и в Ajax-приложениях.

Общие сведения

При взаимодействии, ориентированном на содержимое, HTML-данные генерируются сервером и отображаются составе элемента IFrame, включенного в главную Web-страницу. Элементы IFrame обсуждались в главе 2. Их можно определить в составе HTML-кода страницы или сгенерировать с помощью программы. При программной генерации мы получаем динамический интерфейс, а окно браузера напоминает оконный диспетчер. На рис. 5.7 условно показана архитектура, ориентированная на содержимое.


Рис. 5.7. Архитектура Ajax-приложения, ориентированная на содержимое. Клиент создает элемент IFrame и передает серверу запрос на получение информации. Содержимое генерируется моделью, представлением и контроллером уровня представления и возвращаются элементу IFrame. На уровне клиента никакие требования к модели предметной области не предъявляются

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

Листинг 5.2. Содержимое файла ContentPopup.js
var offset=8;

function showInfo(event){
  var planet=this.id;
  var infoWin=new ContentPopup(
    "info_"+planet+".html",
    planet+"Popup",
    planet,offset,offset,320,320
  );
  offset+=32;
}

function ContentPopup(url,winEl,displayStr,x,y,w,h){

  var bod=document.createElement("div");
  document.body.appendChild(bod);

  this.iframe=document.createElement("iframe");
  this.iframe.className="winContents";
  this.iframe.src=url;
  bod.appendChild(this.iframe);

  this.win=new windows.Window(bod,displayStr,x,y,w,h);
}

Функция showInfo() выполняет роль обработчика события для DOM-элемента, представляющего планету. В обработчике ссылка this указывает на элемент DOM. Чтобы определить, для какой из планет отображаются данные, используется идентификатор элемента.

Мы определили объект ContentPopup, который генерирует универсальный объект Window, создает элемент IFrame, предназначенный для просмотра содержимого, и загружает в него ресурс, определяемый URL. В данной ситуации мы лишь формируем имя статического HTML-файла и оформляем его в виде URL. В более сложном случае, если данные генерируются динамически, нам, возможно придется добавить к URL строку параметров. Простой файл, предназначенный для отображения в составе IFrame (листинг 5.3) генерируется сервером.

Листинг 5.3. Содержимое файла info_earth.html
<html>
<head>
<link rel=stylesheet type="text/css" href="../style.css"/>
</head>
<body class="info">
<div class="framedInfo" id="info">
<div class="title" id="infotitle">earth</div>
<div class="content" id="infocontent">
A small blue planet near the outer rim of the galaxy,
third planet out from a middle-sized sun.
</div>
</div>
</body>
</html>

В данном документе нет ничего примечательного: мы лишь используем обычный HTML-файл, как и для классического Web-приложения.

При использовании архитектуры, ориентированной на содержимое, клиенту требуется лишь ограниченная информация о бизнес-логике приложения, достаточная для размещения элемента IFrame и формирования URL, определяющего содержимое для загрузки. Связь между клиентом и уровнем представления очень слабая; основная ответственность за предоставление содержимого возлагается на сервер. Преимущество данного типа взаимодействия состоит в том, что оно позволяет использовать готовые HTML-документы. Он может оказаться полезен в двух случаях: при включении содержимого, предоставляемого внешними узлами, например, информации, получаемой от деловых партнеров, и при отображении данных, полученных от уже существующих приложений. В этих случаях использование формата HTML может обеспечивать высокую эффективность и как правило не возникает необходимости преобразовывать информацию в другой вид. В качестве примера можно привести документы, содержащие справочную информацию. Использовать в приложениях Ajax подход, ориентированный на содержимое, целесообразно в тех случаях, когда в классических приложениях применяются всплывающие окна. Тем не менее, данная архитектура имеет ограниченное применение. Причины этого нетрудно понять, зная ее недостатки.

Проблемы и ограничения

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

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

Нельзя также забывать проблемы, связанные с практичностью традиционных Web-приложений. Во-первых, каждый запрос на получение содержимого предполагает повторное получение статических данных. Во-вторых, несмотря на то, что при обновлении данных в основном документе автоматически подавляются меры для подавления мерцания, в элементе IFrame этот эффект может присутствовать. Мерцания, правда, можно избежать, реализовав дополнительный код для отображения сообщения “поверх” фрейма.

Итак, в качестве первого термина нашего словаря, описывающего запросы к серверу Ajax, мы записали понятие “ориентированный на содержимое”. Применимость данного подхода ограничена, однако учесть такую возможность все же необходимо. Существует ряд задач, которые нельзя удовлетворительно решить в рамках подхода, ориентированного на содержимое. В качестве примера таких задач можно привести обновление части компонента, например, отдельной пиктограммы или строки таблицы. Единственный способ решить такую задачу — передавать JavaScript-код.

Варианты применения подхода, ориентированного на содержимое

До сих пор, рассматривая архитектуру, ориентированную на содержимое, мы предполагали, что для получения данных используется элемент IFrame. В качестве альтернативного решения можно рассмотреть генерацию фрагмента HTML-кода в качестве реакции на асинхронный запрос и присвоение ответа свойству innerHTML элемента DOM текущего документа. Более подробно мы рассмотрим данное решение в главе 12.

5.4.4 Взаимодействие, ориентированное на сценарий

Когда мы организуем передачу с сервера JavaScript-файла и выполнение этого файла в среде браузера, мы можем утверждать, что решаемая нами задача нетривиальна. Если JavaScript-код, предназначенный для передачи, генерируется программой, то выполняемые действия еще более сложны. Традиционно клиентские и серверные программы обмениваются данными друг с другом. Передача по сети исполняемого кода, обеспечивает дополнительную гибкость. Поддержка мобильного кода реализована лишь в инструментах, предназначенных для создания корпоративных приложений, например, Java и .NET. Для этой цели специально предусмотрены технологии RMI, Jini, и .NET Remoting Framework. Разработчики же простых Web-приложений решают подобные задачи постоянно! Логично предположить, что Ajax расширит наши возможности по работе с мобильным кодом.

Общие сведения

В классическом Web-приложении HTML-код и JavaScript-сценарий обычно располагаются в одном файле и сценарий специально ориентирован на работу с конкретной Web-страницей. Используя Ajax, мы можем загружать сценарии и Web-страницы независимо друг от друга, а это дает возможность модифицировать страницу различными способами, в зависимости от полученного сценария. Коды, составляющие клиентскую часть приложения, могут быть существенно расширены в процессе работы. Это не только предоставляет новые возможности, но и, как вы вскоре увидите, создает проблемы. На рис. 5.8 условно показана архитектура, ориентированная на сценарии.


Рис. 5.8. Архитектура Ajax-приложения, ориентированная на сценарии. Клиентская программа передает серверу запрос на получение фрагмента JavaScript-кода. Полученный код интерпретируется. На стороне клиента обеспечиваются точки входа для сгенерированных сценариев, что позволяет сценарию управлять клиентской программой

Первое преимущество данного подхода по сравнению с архитектурой, ориентированной на содержимое, состоит в том, что активные действия передаются элементам “второго плана”, что предотвращает мигание изображения.

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

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

Теперь вернемся к примеру отображения информации о планетах солнечной системы. В листинге 5.4 показан простой API для отображения окон с данными.

Листинг 5.4. Функция showPopup() и вспомогательный код
var offset=8;

function showPopup(name,description){
  var win=new ScriptIframePopup
    (name,description,offset,offset,320,320);
  offset+=32;
}

function ScriptIframePopup(name,description,x,y,w,h){

  var bod=document.createElement("div");
  document.body.appendChild(bod);

  this.contentDiv=document.createElement("div");
  this.contentDiv.className="winContents";
  this.contentDiv.innerHTML=description;
  bod.appendChild(this.contentDiv);

  this.win=new windows.Window(bod,name,x,y,w,h);
}

Мы определили функцию showPopup(), которая получает два параметра и создает объект окна. В листинге 5.5 приведен пример сценария, вызывающего эту функцию.

Листинг 5.5. Содержимое файла script_earth.js
var name='earth';
var description="A small blue planet near the outer rim of the galaxy,"
  +"third planet out from a middle-sized sun.";

showPopup (name,description);

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

Загрузка сценариев в элементы IFrame

Если мы загрузим JavaScript-код, используя HTML-дескриптор <script>, сценарий будет автоматически обработан интерпретатором. С этой точки зрения элемент IFrame не отличается от обычного документа. Мы можем определить метод showInfo() для создания IFrame и загрузить в него сценарий.

function showInfo(event){
  var planet=this.id;
  var scriptUrl="script_"+planet+".html";
  var dataframe=document.getElementById('dataframe');
  if (!dataframe){
    dataframe=document.createElement("iframe");
    dataframe.className='dataframe';
    dataframe.id='dataframe';
    dataframe.src=scriptUrl;
    document.body.appendChild(dataframe);
  }else{
    dataframe.src=scriptUrl;
  }
}

Средства для обработки DOM уже знакомы вам. Если мы используем для загрузки сценария невидимый элемент IFrame, то можем сосредоточить внимание на самом сценарии, поскольку все остальные действия выполняются автоматически. Включим наш пример сценария в состав HTML-документа, как это показано в листинге 5.6.

Листинг 5.6. Содержимое файла script_earth.html
<html>
<head>
<script type='text/javascript' src='script_earth.js'>
</script>
</head>
<body>
</body>
</html>

Если мы попытаемся загрузить этот код, он не будет работать, так как IFrame формирует собственный JavaScript-контекст и не может непосредственно обратиться к API, определенному в основном документе. Если в составе сценария выполняется приведенное ниже выражение, браузер ищет функцию showPopup() в контексте IFrame.

showPopup(name,description);

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

top.showPopup(name,description);

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

В загружаемом нами сценарии используется функциональный подход. Если нам понадобится создать в IFrame экземпляр объекта, мы столкнемся с новой проблемой. Предположим, что в файле PlanetInfo.js определен объект PlanetInfo, к которому наш сценарий обращается следующим образом.

var pinfo=new PlanetInfo(name,description);

Для того, чтобы такое обращение было возможно. мы должны импортировать PlanetInfo.js в контекст IFrame, указав дополнительный дескриптор <script>.

<script type='text/javascript' src='PlanetInfo.js'></script>
<script type='text/javascript'>
  var pinfo=new PlanetInfo(name,description);
</script>

Объект PlanetInfo, созданный в IFrame будет вести себя так же, как и объект, созданный в фрейме верхнего уровня, но они имеют разные прототипы. Если впоследствии мы удалим элемент IFrame, а документ верхнего уровня сохранит ссылку на объект, созданный в составе IFrame, то последующие обращения к методам объекта станут невозможны. Более того, оператор instanceof даст совершенно неожиданные результаты (они приведены в табл. 5.1).

Элемент, в составе которого был создан объектЭлемент, в составе которого был вызван оператор instanceofРезультаты выполнения оператора instanceof
Документ верхнего уровняДокумент верхнего уровняtrue
Документ верхнего уровняIFramefalse
IFrameДокумент верхнего уровняfalse
IFrameIFrametrue
Таблица 5.1. Результаты применения оператора instanceof

Импортирование определения объекта в различные контексты — не такая простая задача, как это может показаться на первый взгляд. Решить проблему можно, определив в API документа верхнего уровня фабричный метод. Например:

function createPlanetInfo(name,description){
  return new PlanetInfo(name,description);
}

При этом исчезает необходимость иметь ссылку на собственную версию объекта PlanetInfo.

<script type='text/javascript'>
  var pinfo=createPlanetInfo(name,description);
</script>

Функция showPopup() в листинге 5.4 представляет собой фабрику для объекта ScriptIframePopup.

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

Загрузка сценариев с использованием XMLHttpRequest и eval()

Подобно многим языкам сценариев, JavaScript содержит функцию eval(), которая позволяет передавать интерпретатору JavaScript произвольный текст. Некоторые считают, что использование функции eval() замедляет работу приложения. Это действительно так, если eval() систематически вызывается для малых сценариев. Однако, поскольку эта функция доступна нам, мы можем использовать ее для выполнения кода, загруженного с помощью объекта XMLHttpRequest. Если eval() вызывать относительно редко для сценариев большого объема, она обеспечивает приемлемую производительность.

Переписав наш пример для работы с функцией eval(), мы получим следующий код.

function showInfo(event){
  var planet=this.id;
  var scriptUrl="script_"+planet+".js";
  new net.ContentLoader(scriptUrl,evalScript);
}

function evalScript(){
  var script=this.req.responseText;
  eval(script);
}

Теперь метод showInfo() использует объект XMLHttpRequest (помещенный в класс ContentLoader) для получения сценария с сервера; причем необходимость включать его в состав HTML-страницы отпадает. Вторая функция, evalScript(), представляет собой функцию обратного вызова; ссылка на нее передается ContentLoader. Когда функция evalScript() получает управление, может быть прочитано значение свойства responseText, принадлежащего объекту XMLHttpRequest. Весь сценарий выполняется не в отдельном контексте IFrame, а в контексте текущей страницы.

Теперь мы описали новый термин — “ориентированный на сценарий”. Заметим, что данный подход допускает две реализации; посредством IFrame или с помощью eval(). Сравним подход, ориентированный на сценарий, с подходом, ориентированным на содержимое.

Проблемы и ограничения

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

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

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

5.4.5 Взаимодействие, ориентированное на данные

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

Общие сведения

Иногда бывает необходимо организовать использование данных, полученных Ajax-клеинтом, другими программами, например, интеллектуальными клиентами Java или .NET, либо программным обеспечением мобильных телефонов или PDA. В этих случаях JavaScript-инструкции плохо применимы; более уместным был бы “нейтральный” формат данных.

При взаимодействии, ориентированном на данные, сервер передает лишь данные, которые обрабатываются не интерпретатором JavaScript, а самой клиентской программой. Архитектура, ориентированная на данные, условно показана на рис. 5.9.


Рис. 5.9. В системе, ориентированной на данные, сервер в ответ на запрос возвращает поток низкоуровневых данных (представленных, например, в формате XML). Эти данные обрабатываются на уровне клиента и используются для обновления клиентской модели и/или пользовательского интерфейса

Большинство примеров, приведенных в данной книге, созданы в рамках подхода, ориентированного на данные. Наиболее очевидный формат данных — XML, но возможны также и другие форматы.

Использование XML-данных

В настоящее время формат XML применяется очень часто. Среда браузера, в которой выполняется Ajax-приложение и, в частности, объект XMLHttpRequest предоставляет средства поддержки XML-данных. Если объект XMLHttpRequest получает ответ типа application/xml или text/xml, он оформляет его в виде структуры DOM (Document Object Model). В листинге 5.7 приведен код приложения, отображающего данные о планетах, адаптированный для работы с информацией в формате XML.

Листинг 5.7. Содержимое файла DataXMLPopup.js
var offset=8;
function showPopup(name,description){
  var win=new DataPopup(name,description,offset,offset,320,320);
  offset+=32;
}

function DataPopup(name,description,x,y,w,h){

  var bod=document.createElement("div");
  document.body.appendChild(bod);

  this.contentDiv=document.createElement("div");
  this.contentDiv.className="winContents";
  this.contentDiv.innerHTML=description;
  bod.appendChild(this.contentDiv);

  this.win=new windows.Window(bod,name,x,y,w,h);
}

function showInfo(event){
  var planet=this.id;
  var scriptUrl=planet+".xml";
  new net.ContentLoader(scriptUrl,parseXML);
}

function parseXML(){
  var name="";
  var descrip="";
  var xmlDoc=this.req.responseXML;
  var elDocRoot=xmlDoc.getElementsByTagName("planet")[0];
  if (elDocRoot){
    attrs=elDocRoot.attributes;
    name=attrs.getNamedItem("name").value;
    var ptype=attrs.getNamedItem("type").value;
    if (ptype){
      descrip+="<h2>"+ptype+"</h2>";
    }

    descrip+="<ul>";
    for(var i=0;i<elDocRoot.childNodes.length;i++){
      elChild=elDocRoot.childNodes[i];
      if (elChild.nodeName=="info"){
        descrip+="<li>"+elChild.firstChild.data+"</li>\n";
      }
    }
    descrip+="</ul>";
  }else{
    alert("no document");
  }
  top.showPopup(name,descrip);
}

Функция showInfo() обращается к объекту XMLHttpRequest, содержащемуся в составе ContentLoader, и объявляет в качестве функции обратного вызова parseXML(). В данном случае функция обратного вызова сложнее, чем evalScript(), которую мы рассматривали в разделе 5.6.3. Это связано с тем, что нам необходимо поддерживать навигацию по структуре DOM, извлекать данные и вызывать метод showPopup(). В листинге 5.8 содержится пример ответа, сгенерированного сервером в формате XML.

Листинг 5.8. Содержимое файла earth.xml
<planet name="earth" type="small">
  <info id="a" author="dave" date="26/05/04">
    Earth is a small planet, third from the sun
  </info>
  <info id="b" author="dave" date="27/02/05">
    Surface coverage of water is roughly two-thirds
  </info>
  <info id="c" author="dave" date="03/05/05">
    Exhibits a remarkable diversity of climates and landscapes
  </info>
</planet>

Существенным преимуществом XML является тот факт, что информация, представленная в этом формате, естественным образом структурирована. Так в данном примере содержится ряд дескрипторов <info>, которые функция parseXML() преобразует в маркированный HTML-список.

Таким образом, благодаря использованию XML, нам удалось разделить уровни сервера и клиента. Код клиента и сервера можно изменять, независимо один от другого, при условии, конечно, что они будут поддерживать формат документа. Однако подход, ориентированном на сценарий, имел очень важное преимущество: вся работа выполнялась JavaScript-интерпретатором. Рассмотрим решение, предполагающее использование JSON. Оно позволяет объединить преимущества обеих архитектур.

Использование JSON-данных

Имя для объекта XMLHttpRequest было выбрано не совсем удачно. На самом деле он может принимать не только XML, но и любую текстовую информацию. Для передачи данных Ajax-клиенту очень удобен формат JSON (JavaScript Object Notation), так как он позволяет представить в компактном виде граф объектов JavaScript. В листинге 5.9 показано, как можно адаптировать пример приложения, предоставляющего информацию о планетах, для использования JSON.

Листинг 5.9. Содержимое файла DataJSONPopup.js
function showInfo(event){
  var planet=this.id;
  var scriptUrl=planet+".json";
  new net.ContentLoader(scriptUrl,parseJSON);
}

function parseJSON(){
  var name="";
  var descrip="";
  var jsonTxt=net.req.responseText;
  var jsonObj=eval("("+jsonTxt+")");
  name=jsonObj.planet.name
  var ptype=jsonObj.planet.type;
  if (ptype){
    descrip+="<h2>"+ptype+"</h2>";
  }

  var infos=jsonObj.planet.info;
  descrip+="<ul>";
  for(var i in infos){
    descrip+="<li>"+infos[i]+"</li>\n";
  }
  descrip+="</ul>";

  top.showPopup(name,descrip);
}

Здесь мы также загружаем данные с помощью ContentLoader. Функцией обратного вызова в этом случае является parseJSON(). Текст ответа представляет собой JavaScript-выражение, поэтому мы можем создать граф объектов путем вызова eval().

var jsonObj=eval("("+jsonTxt+")");

Заметьте, что перед обработкой выражения нам надо поместить его в скобки. Теперь мы можем обращаться к свойствам объекта по имени, а это позволяет сократить размеры кода и сделать его более удобным для восприятия, чем методы обработки структуры DOM, которые мы использовали при работе с XML. Здесь метод showPopup() не приводится, поскольку он выглядит точно так же, как и в листинге 5.7.

Какой же вид имеют JSON-данные? В листинге 5.10 показана информация о планете Земля, представленная как строка JSON.

Листинг 5.10. Содержимое файла earth.json
{"planet": {
  "name": "earth",
  "type": "small",
  "info": [
    "Earth is a small planet, third from the sun",
    "Surface coverage of water is roughly two-thirds",
    "Exhibits a remarkable diversity of climates and landscapes"
  ]
}}

С помощью фигурных скобок определяются ассоциативные массивы, а квадратные скобки обозначают массивы с числовыми индексами. Допускается вложенность любых видов скобок. В приведенном примере мы определили объект с именем planet, содержащий три свойства. Свойства name и type представляют собой обычные строки, а свойство info является массивом.

Формат JSON используется реже, чем XML, но JSON-данные могут быть обработаны любым интерпретатором JavaScript, включая Mozilla Rhino на базе Java и Microsoft JScript .NET. Библиотеки JSON-RPC содержат средства разбора JSON для различных языков программирования (соответствующие ссылки приведены в конце данной главы), а также инструмент JavaScript “Stringifier”, предназначенный для преобразования JavaScript-объектов в строки JSON. Таким образом, JSON можно рассматривать как формат для двустороннего взаимодействия. Если интерпретатор JavaScript доступен и на стороне клиента и на стороне сервера, целесообразно выбрать формат JSON. В рамках проекта JSON-RPC были также разработаны библиотеки, предназначенные для разбора и генерации JSON-сообщений и ориентированные на языки, которые часто используются при разработке программ, выполняемых на стороне сервера.

Теперь в нашем словаре появился термин “ориентированный на данные”. Кроме того, мы выяснили, что для обмена данными можно использовать не только XML, но и другие форматы.

Использование XSLT

Альтернативой написанию программ для обработки дерева DOM и создания HTML-данных (этот подход был описан в разделе 5.7.3) является использование XSLT-преобразования для автоматического конвертирования XML-информации в формат XHTML. Этот подход представляет собой нечто среднее между взаимодействием, ориентированным на данные и взаимодействием, ориентированным на содержимое. С точки зрения сервера эта архитектура ориентирована на данные, а с точки зрения клиента — на содержимое. Такой подход позволяет быстрее и проще получить требуемый результат, но для него характерны те же ограничения, что и для взаимодействия, ориентированного на содержимое, а именно, данные ответа затрагивают лишь прямоугольную область в составе окна. XSLT-преобразование будет подробнее обсуждаться в главе 11.

Проблемы и ограничения

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

Три рассмотренные нами подхода описывают различные варианты систем: от классического Web-приложения до “толстого” клиента, характерного для настольных систем. К счастью эти три архитектуры не являются взаимоисключающими и могут быть совместно использованы в рамках одного приложения.

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

5.5 Передача данных серверу

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

Рассмотрим сначала обновление с учетом изменений, которые мы вносим сами. Существует два механизма передачи данных на сервер: HTML-формы и объект XMLHttpRequest. Рассмотрим кратко каждый из них.

5.5.1 Использование HTML-форм

В классическом Web-приложении элементы HTML-форм реализуют стандартный механизм ввода данных пользователем. Элементы формы объявляются с помощью средств разметки HTML.

<form method="POST" action="myFormHandlerURL.php">
  <input type="text" name="username"/>
  <input type="password" name="password"/>
  <input type="submit" value="login"/>
</form>

В результате обработки данного фрагмента кода отображаются два пустых поля редактирования. Если пользователь введет в полях значения dave и letmein и активизирует кнопку submit, будет сформирован HTTP-запрос POST и передан ресурсу myFormHandlerURL.php. В теле запроса будет содержаться текст username=dave&password=letmein. В большинстве систем Web-программирования разработчику непосредственно не доступны закодированные данные формы. Вместо этого ему предоставляются пары имя-значения в виде ассоциативного массива или специальных переменных.

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

<form id="myForm" method="POST" action="" 
      onsubmit="validateForm(); return false;">
  <input type="text" name="username"/>
  <input type="password" name="password"/>
  <input type="submit" value="login"/>
</form>

В этом случае функция, выполняющая проверку, будет иметь следующий вид.

function validateForm(){
  var form=document.getElementById('myForm');
  var user=form.elements[0].value;
  var pwd=form.elements[1].value;
  if (user && user.length>0 && pwd && pwd.length>0){
    form.action='myFormHandlerURL.php';
    form.submit();
  }else{
    alert("please fill in your credentials before logging in");
  }

}

Первоначально форма определяется без атрибута action. Обращение по реальному URL будет произведено только в случае положительного результата проверки. С помощью JavaScript-кода можно также модифицировать форму, организовав блокирование кнопки submit для предотвращения повторной передачи данных, кодирование пароля перед формированием запроса и т.д. Эти и многие другие приемы многократно описаны в литературе, поэтому мы не будем рассматривать их здесь. В главах 9 и 10 приведены примеры расширения возможностей HTML-форм.

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

истинг 5.11. Функция submitData()
function addParam(form,key,value){
  var input=document.createElement("input");
  input.name=key;
  input.value=value;
  form.appendChild(input);
}
function submitData(url,data){
  var form=document.createElement("form");
  form.action=url;
  form.method="POST";
  for (var i in data){
    addParam(form,i,data[i]);
  }
  form.style.display="none";
  document.body.appendChild(form);
  form.submit();
}

функция submitData() создает форму и в цикле включает в нее данные, используя для этой цели функцию addParam(). Обращение к функции submitData() имеет следующий вид.

submitData(
  "myFormHandlerURL.php",
  {username:"dave",password:"letmein"}
);

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

5.5.2 Использование объекта XMLHttpRequest

Объект XMLHttpRequest уже рассматривался нами в этой главе, а также в главе 2. С точки зрения клиентского кода разница между чтением и обновлением незначительна. Нам надо лишь указать метод POST и передать параметры формы.

В листинге 5.12 показан код объекта ContentLoader, который мы разработали в разделе 3.1. Мы несколько изменили его для того, чтобы можно было включать в запрос параметры и указывать произвольный HTTP-метод.

Листинг 5.12. Объект ContentLoader
// (111) Для конструктора предусмотрены дополнительные параметры
net.ContentLoader=function
 (url,onload,onerror,method,params,contentType){   
  this.onload=onload;
  this.onerror=(onerror) ? onerror : this.defaultError;
  this.loadXMLDoc(url,method,params,contentType);
}

net.ContentLoader.prototype.loadXMLDoc
  =function(url,method,params,contentType){
  if (!method){
    method="GET";
  }
  if (!contentType && method=="POST"){
    contentType="application/x-www-form-urlencoded";
  }
  if (window.XMLHttpRequest){
    this.req=new XMLHttpRequest();
  } else if (window.ActiveXObject){
    this.req=new ActiveXObject("Microsoft.XMLHTTP");
  }
  if (this.req){
    try{
      this.req.onreadystatechange=net.ContentLoader.onReadyState;
      // HTTP-метод
      this.req.open(method,url,true);   
      // Тип содержимого              
      if (contentType){               
        this.req.setRequestHeader("Content-Type", contentType);
      }
      // Параметры запроса
      this.req.send(params);   
    }catch (err){
      this.onerror.call(this);
    }
  }
}

Конструктору объекта мы передаем несколько новых параметров (111). Из них обязательными являются URL (в соответствии с атрибутом action формы) и обработчик onload. Можно также задать HTTP-метод, параметры запроса и тип запроса. Заметьте, что при передаче пар ключ-значение посредством метода POST надо указать тип содержимого application/x-www-form-urlencoded. Если при вызове функции тип явно не задан, мы устанавливаем это значение автоматически. HTTP-метод используется при вызове метода open() объекта XMLHttpRequest а параметры — при обращении к методу send(). Таким образом, вызов конструктора имеет следующий вид.

var loader=net.ContentLoader(
  'myFormHandlerURL.php', 
  showResponse,
  null,
  'POST',
  'username=dave&password=letmein' 
);

В результате выполнения этого выражения будет формирован такой же запрос, как и при использовании метода submitData(), представленного в листинге 5.11. Заметьте, что параметры передаются в виде строки, которая кодируется так же, как это происходит при передаче данных формы, например:

name=dave&job=book&work=Ajax_In+Action

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

5.5.3 Управление обновлением модели

В главе 3 мы рассмотрели универсальный объект ObjectViewer, предназначенный для представления сложных моделей и обсудили простой пример, в котором данный объект используется для просмотра информации о планетах. Каждый из объектов, представляющих планеты солнечной системы содержит несколько параметров, а некоторые текстовые свойства — диаметр и расстояние от Солнца — мы определили как редактируемые. Изменение любого свойства перехватывается центральной функцией обработки событий, которую мы использовали для представления отладочной информации в строке состояния браузера. (Возможность записывать данные в строку состояния в последних версиях Mozilla Firefox ограничена. В приложении А мы рассмотрим простую консоль, поддерживаемую средствами JavaScript, которая позволяет отображать сообщения о состоянии системы при отсутствии в браузере строки состояния.) Средства обработки событий почти идеально подходят для передачи серверу информации об обновлениях.

Предположим, что на сервере выполняется сценарий updateDomainModel.jsp, который получает от клиента следующую информацию.

Мы можем написать обработчик событий, который передавал бы серверу требуемую информацию.

function updateServer(propviewer){
  var planetObj=propviewer.viewer.object;
  var planetId=planetObj.id;
  var propName=propviewer.name;
  var val=propviewer.value;
  net.ContentLoader(
    'updateDomainModel.jsp', 
    someResponseHandler,
    null,
    'POST',
    'planetId='+encodeURI(planetId)
    +'&propertyName='+encodeURI(propName)
    +'&value='+encodeURI(val)
  );
}

Этот обработчик надо связать с ObjectViewer.

myObjectViewer.addChangeListener(updateServer);

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

Листинг 5.13. Объект CommandQueue
// (111) Создать объект очереди
net.CommandQueue=function(id,url,freq){   
  this.id=id;
  net.cmdQueues[id]=this;
  this.url=url;
  this.queued=new Array();
  this.sent=new Array();
  if (freq){
    this.repeat(freq);
  }
}

// (222) Передать запрос серверу
net.CommandQueue.prototype.addCommand=function(command){
  if (this.isCommand(command)){
    this.queue.append(command,true);
  }
}

net.CommandQueue.prototype.fireRequest=function(){   
  if (this.queued.length==0){
    return;
  }
  var data="data=";
  for(var i=0;i<this.queued.length;i++){
    var cmd=this.queued[i];
    if (this.isCommand(cmd)){
      data+=cmd.toRequestString();
      this.sent[cmd.id]=cmd;
    }
  }
  this.queued=new Array();
  this.loader=new net.ContentLoader(
    this.url,
    net.CommandQueue.onload,net.CommandQueue.onerror,
    "POST",data
  );
}

// (333) Проверить тип объекта
net.CommandQueue.prototype.isCommand=function(obj){   
  return (
    obj.implementsProp("id")
    && obj.implementsFunc("toRequestString")
    && obj.implementsFunc("parseResponse")
  );
}

// (444) Выполнить разбор ответа сервера
net.CommandQueue.onload=function(loader){   
  var xmlDoc=net.req.responseXML;
  var elDocRoot=xmlDoc.getElementsByTagName("commands")[0];
  if (elDocRoot){
    for(i=0;i<elDocRoot.childNodes.length;i++){
      elChild=elDocRoot.childNodes[i];
      if (elChild.nodeName=="command"){
        var attrs=elChild.attributes;
        var id=attrs.getNamedItem("id").value;
        var command=net.commandQueue.sent[id];
        if (command){
          command.parseResponse(elChild);
        }
      }
    }
  }

}
net.CommandQueue.onerror=function(loader){
  alert("problem sending the data to the server");
}

// (555) Опрос сервера
net.CommandQueue.prototype.repeat=function(freq){   
  this.unrepeat();
  if (freq>0){
    this.freq=freq;
    var cmd="net.cmdQueues["+this.id+"].fireRequest()";
    this.repeater=setInterval(cmd,freq*1000);
  }
}
// (666) Отключить опрос сервера
net.CommandQueue.prototype.unrepeat=function(){   
  if (this.repeater){
    clearInterval(this.repeater);
  }
  this.repeater=null;
}

При инициализации (111) объекта CommandQueue (он назван так потому, что в очереди содержатся объекты Command) указывается уникальный идентификатор, URL сценария на стороне сервера и в качестве необязательного параметра — флаг, указывающий на необходимость повторного опроса. Альтернативный вариант — активизация опроса вручную. Каждый из этих режимов может быть полезен, поэтому оба они предусмотрены в коде объекта. Когда очередь формирует запрос серверу, все содержащиеся в ней команды преобразуются в строки и передаются серверу (222).

В составе объекта содержатся два массива. Массив queued предполагает указание числовых индексов; в него помещаются новые обновления. Массив sent — ассоциативный. Он содержит те обновления, которые были отправлены серверу, но ответы на которые еще не были получены. В обоих массивах содержатся объекты Command, интерфейс которых определяется функцией isCommand() (333). Эти объекты обладают следующими свойствами.

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

Object.prototype.implementsFunc=function(funcName){
  return this[funcName] && this[funcName] instanceof Function;
}

Прототипы JavaScript подробно описаны в приложении Б. Теперь вернемся к объекту, реализующему очередь. Метод onload очереди (444) ожидает ответа сервера, содержащего XML-документ. В составе документа должны присутствовать дескрипторы <command>, помещенные в дескриптор <commands>.

Методы repeat() (555) и unrepeat() (666) используются для управления объектом таймера при периодическом опросе сервера.

Объект Command, предназначенный для обновления свойств объекта, описывающего планету, показан в листинге 5.14.

Листинг 5.14. Объект UpdatePropertyCommand
planets.commands.UpdatePropertyCommand=function(owner,field,value){
  this.id=this.owner.id+"_"+field;
  this.obj=owner;
  this.field=field;
  this.value=value;
}

planets.commands.UpdatePropertyCommand.toRequestString=function(){
  return {
    type:"updateProperty",
    id:this.id,
    planetId:this.owner.id,
    field:this.field,
    value:this.value
  }.simpleXmlify("command");
}

planets.commands.UpdatePropertyCommand.parseResponse=function(docEl){
  var attrs=docEl.attributes;
  var status=attrs.getNamedItem("status").value;
  if (status!="ok"){
    var reason=attrs.getNamedItem("message").value;
    alert("failed to update "
     +this.field+" to "+this.value
     +"\n\n"+reason);
  }
}

Данный объект предоставляет уникальный идентификатор команды и инкапсулирует параметры, необходимые серверу. Функция toRequestString() оформляет сама себя в виде фрагмента XML-кода, используя специальную функцию, которую мы присоединили к прототипу Object.

Object.prototype.simpleXmlify=function(tagname){
  var xml="<"+tagname;
  for (i in this){
    if (!this[i] instanceof Function){
      xml+=" "+i+"=\""+this[i]+"\"";
    }
  }
  xml+="/>";
  return xml;
}

В результате создается простой XML-дескриптор (для удобства восприятия он отформатирован вручную).

<command type='updateProperty'
  id='001_diameter' 
  planetId='mercury' 
  field='diameter' 
  value='3'/>

Заметьте, что уникальный идентификатор формируется из идентификатора планеты и имени свойства. Мы не можем передать серверу несколько вариантов одного и того же значения. Если перед передачей содержимого очереди мы насколько раз отредактируем свойство, каждое последующее значение будет заменять предыдущее.

В составе запроса POST, передаваемого серверу, может содержаться один или несколько описанных выше дескрипторов, в зависимости от частоты опроса и активности пользователя. Сервер обрабатывает каждую команду и сохраняет результаты, формируя ответ. Обработчик onload, принадлежащий CommandQueue, распознает дескрипторы в составе ответа, сравнивает их с объектами Command в массиве sent и вызывает метод parseResponse() объекта Command. Ответ может выглядеть следующим образом.

<commands>
  <command id='001_diameter' status='ok'/>
  <command id='003_albedo' status='failed' message='value out of range'/>
  <command id='004_hairColor' status='failed' message='invalid property name'/>
</commands>

Как видно из сообщения, информация о диаметре Меркурия была обновлена, но другие два обновления отклонены. Причины описаны посредством атрибута message. Пользователь оповещается о возникающих проблемах (обычно для этого используется функция alert()) и может предпринять ответные действия.

Компонент, обрабатывающий запросы на стороне сервера, должен выделять из данных запроса отдельные команды и передавать каждую команду соответствующему объекту обработчика. По мере обработки команд, результаты записываются в HTTP-ответ. Код простого Java-сервлета, предназначенного для решения описанной задачи, представлен в листинге 5.15.

Листинг 5.15. Содержимое файла CommandServlet.java
public class CommandServlet extends HttpServlet {

  private Map commandTypes=null;
  
  public void init() throws ServletException {
    ServletConfig config=getServletConfig();
    // (111) Конфигурация обработчиков
    commandTypes=new HashMap();   
    boolean more=true;
    for(int counter=1;more;counter++){
      String typeName=config.getInitParameter("type"+counter);
      String typeImpl=config.getInitParameter("impl"+counter);
      if (typeName==null || typeImpl==null){
        more=false;  
      }else{
        try{
          Class cls=Class.forName(typeImpl);
          commandTypes.put(typeName,cls);
        }catch (ClassNotFoundException clanfex){
          this.log(
            "couldn't resolve handler class name "
            +typeImpl);
        }
      }
    }
  }

  protected void doPost(
    HttpServletRequest req, 
    HttpServletResponse resp
  ) throws IOException{
    // (222) Обработать запрос
    resp.setContentType("text/xml");   
    Reader reader=req.getReader();
    Writer writer=resp.getWriter();
    try{
      SAXBuilder builder=new SAXBuilder(false);
      // (333) Обработать XML-данные
      Document doc=builder.build(reader);   
      Element root=doc.getRootElement();
      if ("commands".equals(root.getName())){
        for(Iterator iter=root.getChildren("command").iterator();
        iter.hasNext();){
          Element el=(Element)(iter.next());
          String type=el.getAttributeValue("type");
          XMLCommandProcessor command=getCommand(type,writer);
          if (command!=null){
            // (444) Делегировать обработку
            Element result=command.processXML(el);   
            writer.write(result.toString());
          }
        }
      }else{
        sendError(writer,
          "incorrect document format - "
          +"expected top-level command tag");
      }
    }catch (JDOMException jdomex){
      sendError(writer,"unable to parse request document");  
    }
  }

private XMLCommandProcessor getCommand
  (String type,Writer writer) 
  throws IOException{            
    // (555) Соответствие обработчика команде
    XMLCommandProcessor cmd=null; 
    Class cls=(Class)(commandTypes.get(type));
    if (cls!=null){
      try{
        cmd=(XMLCommandProcessor)(cls.newInstance());
      }catch (ClassCastException castex){
         sendError(writer,
           "class "+cls.getName()
           +" is not a command");
      } catch (InstantiationException instex) {
         sendError(writer,
           "not able to create class "+cls.getName());
      } catch (IllegalAccessException illex) {
         sendError(writer,
          "not allowed to create class "+cls.getName());
      }
    }else{
      sendError(writer,"no command type registered for "+type);  
    }
    return cmd;
  }`

  private void sendError
   (Writer writer,String message) throws IOException{
    writer.write("<error msg='"+message+"'/>");
    writer.flush();
  }
}

Данный сервлет поддерживает карту объектов XMLCommandProcessor, для конфигурирования которой используется интерфейс ServletConfig (111). При использовании более мощных базовых средств можно воспользоваться для этой цели конфигурационным XML-файлом. При обработке запроса POST (222) для разбора XML-данных используется JDOM (333), а затем осуществляется перебор дескрипторов <command>, для которых атрибуты соответствуют объекту обработчика XMLCommandProcessor (444). В карте содержатся определения классов, на основе которых в методе getCommand() мы создаем конкретные экземпляры, используя механизм отражения (555).

В интерфейсе XMLCommandProcessor объявлен единственный метод.

public interface XMLCommandProcessor {
   Element processXML(Element el);
}

В данном интерфейсе предполагается, что для представления XML-данных будут применяться библиотеки JDOM. Объектами Element являются как параметры, так и возвращаемое значение. Простой класс, реализующий данный интерфейс и предназначенный для обновления данных о планетах, показан в листинге 5.16.

Листинг 5.16. Содержимое файла PlanetUpdateCommandProcessor.java
public class PlanetUpdateCommandProcessor
  implements XMLCommandProcessor {

  public Element processXML(Element el) {
    // (111) Создать XML-узел результатов
    Element result=new Element("command");   
    String id=el.getAttributeValue("id");
    result.setAttribute("id",id);
    String status=null;
    String reason=null;
    String planetId=el.getAttributeValue("planetId");
    String field=el.getAttributeValue("field");
    String value=el.getAttributeValue("value");
    // (222) Обращение к модели предметной области
    Planet planet=findPlanet(planetId);   
    if (planet==null){
      status="failed";
      reason="no planet found for id "+planetId;
    }else{
      Double numValue=new Double(value);
      Object[] args=new Object[]{ numValue };
      String method = "set"+field.substring(0,1).toUpperCase()
        +field.substring(1);
      Statement statement=new Statement(planet,method,args);
      try {
        // (333) Обновить модель предметной области
        statement.execute();   
        status="ok";
      } catch (Exception e) {
        status="failed";
        reason="unable to set value "+value+" for field "+field;
      }
    }
    result.setAttribute("status",status);
    if (reason!=null){
      result.setAttribute("reason",reason);
    }
    return result;
  }

  private Planet findPlanet(String planetId) {
    // (444) Использовать ORM для модели предметной области
    return null;
  }

}

JDOM используется не только для разбора XML-данных, содержащихся в запросе, но и для генерации ответа. Корневой узел (111) и дочерние узлы создаются в методе processXML(). Для доступа к модели предметной области на стороне сервера используется метод findPlanet() (222); ему передается идентификатор объекта. Реализация метода findPlanet() здесь не рассматривается; заметим лишь, что в нем реализуется обмен с базой данных традиционными способами (444). Для обновления модели предметной области (333) применяется механизм отражения.

Так в общих чертах выглядит архитектура, основанная на использовании очереди и используемая для объединения частных действий по обновлению модели предметной области в одну HTTP-транзакцию. Она позволяет как синхронизировать модели на стороне клиента и на стороне сервера, так и эффективно управлять трафиком. Как было сказано в разделе 5.3, такое решение может быть использовано совместно с JSF и другими подобными средствами, когда структура пользовательского интерфейса и модель взаимодействия существенно зависят от сервера. В данном случае мы лишь реализовали эффективный способ обновления модели предметной области на разных уровнях.

На этом мы заканчиваем разговор о средствах обеспечения взаимодействия между клиентом и сервером и обзор основных элементов, определяющих структуру Ajax-приложений. По сути мы заложили основы языка запросов к серверу в рамках Ajax-приложения и получили представление о возможностях, доступных для его реализации.

5.6 Резюме

Мы начали данную главу с рассмотрения основных функций сервера в составе Ajax-приложения, а именно доставки клиентского кода браузеру и предоставления клиенту данных в процессе работы. Рассматривая эти вопросы, мы учли наиболее популярные языки и инструменты, используемые для создания серверных программ. Они ориентированы на классические Web-приложения и мы выяснили, насколько они подходят для Ajax. Инструменты, предназначенные для создания серверных программ, очень быстро изменяются, поэтому вместо того, чтобы обсуждать конкретные продукты, мы разделили их на категории в зависимости от используемой архитектуры. В результате было выделено три основных направления: архитектура Model2, средства на основе компонентов и архитектура, ориентированная на использование служб. На сегодняшний день есть основания считать, что лучше всего для Ajax подходит SOA, однако попытки адаптировать другие архитектуры также могут увенчаться успехом.

Далее мы выделили три основных подхода к решению задачи доставки данных от сервера клиенту: ориентированный на содержимое, ориентированный на сценарий и ориентированный на данные. Если классическое Web-приложение больше всего соответствует архитектуре, ориентированной на содержимое, то приложение Ajax больше укладывается в схему, ориентированную на данные. Взаимодействие, ориентированное на сценарии, занимает промежуточную позицию. Обсуждая архитектуру, ориентированную на данные, мы выяснили, что XML — не единственно возможный формат представления данных. В некоторых случаях при доставке информации клиенту может успешно применяться формат JSON.

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

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

5.7 Ресурсы

В данной главе были упомянуты некоторые инструменты разработки. Ниже приведены их URL.

По данным исследований (http://wicket.sourceforge.net/Introduction.html), в настоящее время только для Java существует более 60 наборов базовых средств.

JSF — это категория инструментов, в которую входят различные продукты. Кито Манн (Kito Mann), автор книги JavaServer Faces in Action (Manning, 2004), поддерживает Web-портал, посвященный JSF (http://www.jsfcentral.com/). Материалы, подготовленые Грегом Мюрреем (Greg Murray) и его коллегами в ходе обсуждения тем Ajax и JSF, доступны по адресу https://bpcatalog.dev.java.net/nonav/ajax/jsf-ajax/frames.html. AjaxFaces представляет собой реализацию JSF для Ajax (http://www.ajaxfaces.com); этот продукт распространяется на коммерческой основе. Готовится модификация Apache Open Source MyFaces (http://myfaces.apache.org/sandbox/inputSuggestAjax.html) в соответствии с требованиями Ajax.

На момент написания данной книги Microsoft Atlas находился в процессе разработки, но первые реализации скоро должны быть доступны. Менеджер проекта Atlas, Скотт Гатри (Scott Guthrie) публикует материалы, имеющие отношение к Ajax (http://weblogs.asp.net/scottgu/archive/2005/06/28/416185.aspx).

Библиотеки JSON-RPC, ориентированные на различные языки программирования, находятся по адресу http://www.json-rpc.org/impl.xhtml.


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