Сообщений 9    Оценка 1 [+0/-1]         Оценить  
Система Orphus

Разработка распределённого Web-приложения

Автор: Хохряков Игорь Александрович
Опубликовано: 21.05.2012
Исправлено: 10.12.2016
Версия текста: 1.1
Необходимость разработки распределённого Web-приложения
Предлагаемая реализация общей архитектуры распределённого веб-приложения
Клиент
Сервер
Положительные особенности предлагаемой реализации
Некоторые особенности реализации
Разработка клиентской части.
Разработка серверной части
Заключение
Список литературы

Необходимость разработки распределённого Web-приложения

Основными направлениями развития сети Интернет последние годы являются: с одной стороны – стремительный рост интернет-аудитории; с другой – всё возрастающее количество предложений различных сервисов в сети Интернет.

Если говорить о первом направлении, то достаточно обратиться к докладу федерального агентства по печати и массовым коммуникациям «ИНТЕРНЕТ В РОССИИ СОСТОЯНИЕ, ТЕНДЕНЦИИ И ПЕРСПЕКТИВЫ РАЗВИТИЯ» от 2011 года [doklad], в котором говориться, что в России к 2014 г. ожидается двукратный прирост аудитории сети Интернет. Естественно, что аналогичная тендеция будет и в мире, взять хотя бы растущие рынки Китая и Индии.

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

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

Всё большее количество пользователей приводит к всё более возрастающей нагрузке на приложение. Одним из современных способов решения данной проблемы является использования распределённых кластерных систем. Сегодня кластерные решения становятся доступны всё более широкому кругу разработчиков. Например, Amazon предоставляет широкий спектр услуг: от доступа к распределенному хранилищу данных до возможности сконфигурировать собственный кластер из производительных компьютеров. Широкое распространение также получают различные платформы и фреймворки для распределённых вычислений, например, Hadoop. Использование распределённых приложений для реализации сервис-ориентированных приложений позволяет существенно повысить пропускную способность приложения, в смысле одновременной обработки запросов большого количества пользователей.

Попытаемся определить, что, собственно, такое «распределённое веб-приложение».

Под веб-приложением в статье понимается клиент-серверное приложение, в котором клиентом выступает браузер. Под распределенным понимается приложение, которое исполняется на кластере или Grid-сети машин.

Исходя из вышесказанного, в данной статье под «распределённым веб-приложением» понимается веб-приложение, серверная часть которого исполняется на кластере.

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

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

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

Под stateless-серверной частью понимается, что на сервере не сохраняются промежуточные данные, а только конечное их состояние. Поясню на примере онлайн-магазина: традиционно корзина пользователя хранится на сервере, содержимое её периодически обновляется через запросы к серверу, в предлагаемой реализации корзина хранится на клиенте, и данные из неё отправляются на сервер только в момент оформления заказа. Это помогает существенно упростить логику обработки запросов на сервере. Также возрастает прозрачность структуры состояний приложения, так как теперь вся логика по обработке данных сконцентрирована на клиенте.

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

Клиент

В качестве клиентской части предлагается использовать JavaScriptMVC [jsMVC]. Это распространяемый в открытом доступе фреймворк, позволяющий изрядно упорядочить и упростить разработку клиентской части приложения.

Помимо удачной реализации паттерна Model-View-Controller (MVC) [mvc], этот фреймворк предлагает поэтапный подход к разработке. Определены четыре этапа: разработка, тестирование, сжатие, эксплуатация. В состав JavaScriptMVC входит утилита командной строки. С помощью этой утилиты разработчики могут генерировать части приложения, запускать юнит-тесты и выполнять компрессию клиентской части приложения. Тем самым становится возможно полностью автоматизировать процесс тестирования и компрессии. Внедрив эти этапы в стандартный жизненный цикл разработки приложения, разработчики могут работать в привычном ритме. Фреймворк также реализует возможность автоматизированного функционального тестирования и генерирования документации на манер JavaDoc.

Подробно ознакомиться с реализацией паттерна MVC и принципами, заложенными в JavaScriptMVC, можно на странице проекта. Стоит также отметить, что проект снабжён простой, но, тем не менее, объёмной документацией, так что фреймворк быстро и лёгко изучается.

Также JavaScriptMVC позволяет полностью достичь принципа stateless для серверной части, поскольку вся клиентская часть приложения может быть реализована в рамках единственной html-страницы. Изменение содержания страницы происходит динамически, с использованием представлений, порождающих динамический html-код. Коммуникация с сервером осуществляется согласно технологии Web 2.0 [web20], т.е. не требует перезагрузки страницы и, соответственно, сохраняются данные на клиенте между запросами.

Фактически, при таком подходе, между клиентом и сервером остаётся только тонкая связь, определяемая формой и содержанием обмениваемых данных. Стоит отметить, что JavaScriptMVC позволяет при тестировании имитировать отклик сервера. Таким образом, договорившись на начальном этапе о протоколе обмена данными, можно полностью отделить разработку клиентской части от разработки серверной.

Во фреймворке реализована поддержка следующих подходов и форматов: Ajax, json_p, json_rest, xml_rest. В демонстрационном приложении используется технология JSONP (JSON with Padding) [jsonp01].

JSONP – паттерн использования JSON для взаимного обмена данными между клиентом и сервером. Идея заключается в «оборачивании» некоторых данных (ответа сервера в формате JSON) вызовом JavaScript-функции. Данная функция будет вызвана на клиенте, как только он получит ответ от сервера, в англоязычной литературе такая функция называется функцией обратного вызова (callback function [jsonp02]).

JSONP реализуется в JavaScriptMVC классическим образом – через динамическое добавление тега script в DOM. Таким образом, если клиентская часть приложения является защищённым ресурсом на сервере, т.е. доступ к ней требует авторизации клиента, отпадает необходимость ручного добавления заголовков авторизации, как, например, в случае Ajax HTTP-запросов.

Сервер

Серверная часть реализуется с помощью servlet технологии [servlets]. Для простоты в демонстрационном приложении используются непосредственно сервлеты для обработки запросов, естественно, в более «серьёзном» приложении можно воспользоваться, например, фреймворком Spring для организации серверной логики.

В качестве сервлет-контейнера используется Apache TomCat [tomcat]. В TomCat реализована возможность работы в режиме кластера, это достигается за счёт репликации данных сессии пользователя между узлами кластера. TomCat используется как базовая реализация сервера приложений во многих других проектах, например, в JBoss или Voldemort.

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

Для хранения данных предлагается использовать реализацию NoSQL-хранилища Voldemort [voldemort]. Voldemort представляет из себя распределённое удалённое хранилище с записями вида ключ-значение.

Далее предполагается, что сервер запущен в режиме кластера. Процесс настройки серверной части на работу в кластере чрезвычайно прост и подробно изложен в документации к TomCat [tomcat].

Положительные особенности предлагаемой реализации

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

Клиентская часть полностью отделена от серверной. Предлагаемый к использованию JavaScriptMVC позволяет выполнить декомпозиции клиента приложения за счёт удачной реализации паттерна MVC. Также становится возможным существенно ускорить процесс разработки за счёт реализации этапов разработки и средств автоматизации тестирования и сборки.

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

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

Некоторые особенности реализации

Ниже будут продемонстрированы некоторые особенности предлагаемой в статье реализации распределённого Web-приложения «Онлайн адресная книга». В этом приложении пользователю предоставляется возможность заходить в систему со своими логином и паролем, добавлять в систему записи с адресами друзей, находить записи по заданному критерию, удалять ненужные записи.

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

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

Использованные в статье примеры кода взяты из созданного автором публичного проекта. Полный код приложения можно скачать (или склонировать) с bitbucket.org: https://bitbucket.org/Ingvord/online-address-book

Далее в тексте подразумевается несколько соглашений по умолчанию. В частности: используется JavaScriptMVC версии 1.5 [jsMVC15]; как клиентская, так и серверная часть разрабатываются в рамках одного Maven-проекта [mvn]. Местоположение проекта на жестком диске адресуется через переменную {ROOT}. Структура проекта имеет следующий вид:

{ROOT}/
             Src/
                   Main/
                           Java/
                           JavaScriptMVC-1.5/
                   Test/
                   Webapp/
                                 WEB-INF/
                           ...

Под {JMVC_ROOT} в данной структуре папок будем понимать папку {ROOT}/src/main/JavaScriptMVC-1.5.

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

Разработка клиентской части.

Настройка окружения клиента приложения.

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

JavaScripMVC можно скачать с официального сайта: http://1-5.javascriptmvc.com/ (версия 1.5) [jsMVC15]. Фреймворк распространяется в zip-архиве, который распаковывается в папку {ROOT}/src/main/javascriptmvc-1.5 (={JMVC_ROOT}) Maven-проекта.

Рассмотрим структуру папок JavaScriptMVC:

Apps – содержит главные скрипты, а также папки с приложениями. Принято, чтобы название главного скрипта приложения совпадало с названием самого приложения.

Controllers – содержит контроллеры приложения. Для удобства здесь можно создать подпапку, например, с именем, соответствующим названию приложения, и помещать туда все контроллеры, относящиеся к этому приложению.

Docs – здесь находиться документация автогенерируемая при компрессии приложения. Её можно размещать на каком-нибудь портале разработчиков данного приложения.

Jmvc – папка, содержащая собственные файлы фреймворка.

Models – модели приложения. Так же, как и с контроллерами, имеет смысл создать здесь подпапку для организации моделей приложений.

Resources – сюда можно помещать все сторонние javascript-библиотеки.

Stylesheets – этой папки не существует изначально, её необходимо создать. В эту папку помещаются все каскадные стили клиента.

Test – набор автоматических тестов приложения. JavaScriptMVC реализует две возможности для тестирования: unit-тестирование и функциональное тестирование. Для имитации отклика сервера используется специальная заглушка – «fixture».

Views – в этой папке находятся файлы с расширением ejs. Эти файлы являются обычными HTML-файлами со встроенным в него JavaScript-кодом. JavaScript-код заключается в специальные теги, например: <% foo() %>, или <%= foo() %>, подробнее это будет рассмотрено ниже, в разделе «Представление».

Корневая директория содержит два очень важных файла: js и js.bat. Это утилиты командной строки для *nix- и Windows-систем, соотвественно. С помощью этих утилит разработчики могут автоматизировать процесс создания отдельных частей приложения (контроллеров, моделей), а также автоматизировать процесс сжатия и тестирования приложения. Так, можно написать ant-скрипт[ant], который будет чистить, тестировать, сжимать приложение и копировать его в заданное место. Такой ant-скрипт можно встроить в maven при помощи ant-plugin для maven [antrun], тем самым унифицируя жизненный цикл приложения. Ниже приведён фрагмент ant-скрипта для компрессии приложения на платформе Windows:

<target name="compile-javascript">
        <echo>Compressing javascript...</echo>
        <echo>Executing: js.bat apps/${appName}/compress.js</echo>
        <exec dir="${src}"executable="cmd">
            <arg line="/c"/>
            <arg line="js.bat"/>
            <arg line="apps/${appName}/compress.js"/>
        </exec>
    </target>

В реализацию JavaScriptMVC заложена поддержка нескольких этапов разработки. В частности, разработчик, через параметр загрузки приложения, может определять следующие этапы: разработка, тестирование, сжатие, на этом этапе все использованные файлы javascript сжимаются в один файл, который потом используется при эксплуатации приложения, эксплуатация. Этот параметр задаётся на главной странице приложения (apps/{app-name}/index.html), через определение параметра src тега script внизу страницы. Собственно, через определение этого тега происходит интеграция JavaScriptMVC на страницу приложения, принято помещать этот тег перед закрывающим тегом body:

<script language="javascript"type="text/javascript"src="../../jmvc/include.js?AddressBook,development"></script>

После того, как мы распаковали JavaScriptMVC и ознакомились со структурой папок, а также написали ant-скрипт, автоматизирующий процесс тестирования и развёртки, настройка окружения для разработки клиентской части завершена. Теперь можно приступить непосредственно к разработке.

Сгенерируем приложение «AddressBook», выполнив команду из директории {JMVC_ROOT} «js jmvc/generate/app AddressBook». В результате будет создана папка {JMVC_ROOT}/apps/AddressBook и главный скрипт приложения {JMVC_ROOT}/apps/AddressBook.js. Ниже приведён код главного скрипта приложения после генерации:

include.css();
include.resources();
include.plugins(
    'controller','controller/scaffold',    'view','view/helpers',    'dom/element',    'io/ajax',    'model/json_rest','model/xml_rest'
);

include(function()
{ 
// исполняется после загрузки перечисленного в предыдущих директивах include
    include.models();
    include.controllers();
    include.views();
});

Основная задача данного скрипта – подгружать необходимые ресурсы в приложение. Ресурсы в широком смысле: от каскадных стилей до определённых в приложении представлений. В режимах development и test JavaScript-ресурсы подгружаются путём добавления тегов скрипта в body. При компрессии все JavaScript-ресурсы «сжимаются» в файл {JMVC_ROOT}/apps/{app_name}/production.js. Соответственно, при эксплуатации приложения подгружается единственный JavaScript-файл – production.js.

Кратко рассмотрим каждый вызов функции include:

.css(... pathToCss) – подгружает каскадные стили. В качестве аргумента ожидается относительный путь к файлу css в папке {JMVC_ROOT} без расширения. Например, «styles» будет соответствовать файлу {JMVC_ROOT}/stylesheets/styles.css. Можно передавать несколько значений, разделяя их запятыми (сигнатура функции, аналогичная множественному параметру в Java).

.resources(... pathToJs) – подгружает js-файлы. Ожидается, что файлы находятся в папке {JMVC_ROOT}/resources.

.plugins(... pluginName) – подгружает необходимые плагины из {JMVC_ROOT}/jmvc/plugins. Про разработку плагинов будет отдельно сказано ниже.

.models(... modelName) – подгружает модели из {JMVC_ROOT}/models.

.controllers(... modelName) – подгружает контроллеры из {JMVC_ROOT}/controllers.

.views(... modelName) – подгружает представления из {JMVC_ROOT}/views. Для корректной работы представлений в разных режимах (development, production) в значения аргументов функции необходимо включать папку ‘views’, например, ‘views/Addresses/create’, в отличие, например, от моделей – ‘Address’ (не ‘models/Address’).

Так как мы будем использовать модели типа jsonp, то нам необходимо внести соответствующие изменения в вызов функции .plugins:

...
include.plugins(
    'controller','controller/scaffold',    'view','view/helpers',    'dom/element',    'io/ajax','model/jsonp'
);
...

В частности, нужно удалить 'model/json_rest','model/xml_rest', и вместо них добавить 'model/jsonp'.

Модель

В реализации JavaScriptMVC модель является обёрткой вокруг используемого протокола взаимодействия клиента и сервера. В версии 1.5 поддерживаются следующие протоколы: ajax, cookie, json_p, json_rest, xml_rest. Естественно пользователь может определить свою модель, например, для локальной файловой системы.

Скелет для новой модели можно сгенерировать при помощи js-утилиты командной строки: «js jmvc/generate/model [TYPE] [NAME]», где TYPE – одно из значений поддерживаемых протоков общения клиента и сервера, NAME – имя модели.

Только что созданную модель необходимо добавить в главный скрипт приложения {JMVC_ROOT}/apps/AddressBook.js:

...
include(function(){ //runs after prior includes are loaded
include.models('Address');
include.controllers();
include.views();
});

Так необходимо поступать с каждым используемым в приложении ресурсом. Например, используемые каскадные стили добавляются функцией .css, сторонние JavaScript-библиотеки – .resources итд.

Результатом работы генератора модели является следующий код:

js jmvc/generate/model json_p Address -> {JMVC_ROOT}/models/Address.js:

Address = MVC.Model.JsonP.extend('Address',
/* @Static */
{},
/* @Prototype */
{}
);

В секции /* @Static */ задаются методы и свойства, доступные через «ссылку класса» – глобальную переменную, в нашем случае это – Adress. Для любой модели, реализованной поверх транспорта, необходимо определить статическое свойство domain:

...
/* @Static */
{
    domain: ‘http://my-server.com’
}
...

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

Важно, чтобы этот объект инициализировался до инициализации модели. Этого можно добиться, если подгрузить соответствующий js-файл через функцию include.resources() в начале главного скрипта приложения, или добавить блок script до блока с инициализацией jmvc на главной странице приложения. Этот способ удобнее тем, что в процессе эксплуатации главная страница приложения может динамически создаваться на сервере, и тем самым подставляться необходимое значение адреса сервера. Это будет показано на примере ниже в разделе о разработке серверной части.

Обращение к статическим свойствам модели, на примере свойства domain, будет выглядеть следующим образом: Adress.domain.

Также важно переопределить статитические свойства attributes и default_attributes:

...
/* @Static */
{
    domain: ‘http://my-server.com’,
    attributes: {      name: ‘string’,      phone: ‘PhoneNumber’},    default_attributes:{}
}
...

Иначе они «унаследуются» от базовой модели, это фактически означает, что изменения внутри одной модели будут видны остальным моделям в приложении.

ПРИМЕЧАНИЕ

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

Определение attributes и default_attributes ещё необходимо из-за того, что на их основе формируется «чистое» представление инстанса данной модели, т.е. содержащее только предметные атрибуты. Так, конкретный экземпляр модели содержит в себе все методы, определённые в этой модели, а также унаследованные методы и свойства (см. рис. 1.а.). «Чистое» представление экземпляра модели можно получить, вызвав на этом экземпляре метод .attributes(). При этом будет возвращён объект JavaScript, содержащий только те атрибуты, которые указаны в этих статических свойствах. Результат представлен на рис. 1.б.


Рис. 1. Содержание экземпляра модели: а – как видно, помимо основных атрибутов (name, phone) экземпляр также содержит ссылки на все методы модели, а также поля, определённые в базовой модели JavaScripMVC (например, errors); б – результат вызова метода .attributes() на конкретном инстансе модели.

Рассмотрим теперь секцию /* @Prototype */. Здесь описываются методы и свойства конкретного экземпляра данной модели. Пример – упомянутый выше метод attributes(), или какой-либо метод, необходимый для обработки конкретного экземпляра модели. Применительно к нашему приложению таким методом может быть метод, возвращающий телефонный код города:

...
/* @Prototype */
{
    getAreaCode: function(){        return this.phone.area;    }
}
...

Значениями атрибута модели могут также быть экземпляры других моделей, определённых в приложении. Такая возможность реализуется в JavaScriptMVC с помощью механизма ассоциаций. Ассоциации определяются через статические методы модели .belong_to(className) и .has_many(className), или непосредственным добавлением значения className в статический массив _associations:

...
/* @Static */
{
    domain: ‘http://my-server.com’,
    attributes: {
      name: ‘string’,
      phone: ‘PhoneNumber’
},
    default_attributes:{},
    _associations:[‘PhoneNumber’]
}
...

Значение параметра className используется далее при добавлении атрибута через статический метод модели .add_attribute(attrName, className) или, как в примере выше, непосредственное его описание в статическом свойстве модели attributes.

ПРИМЕЧАНИЕ

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

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

Сначала нам необходимо создать конкретный экземпляр. Сделать это можно двумя способами: во-первых, через статический метод create(attributeValues, jsonpCallbacks):

...
var insts;
Adress.create({name:”John”,phone:{area:555,number:1234}},{onSucess:function(data){
    insts = data
}})
...

Либо через конструктор:

...
var insts = new Adress({name:”John”,phone:{area:555,number:1234}})
...
insts.save();
...

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

Отдельно стоит отметить, что модель можно сгенерировать во время исполнения. Это достигается использованием статических методов базовой модели. В реализации библиотеки MVC.Class, используемой для имитации наследования в JavaScriptMVC, статические методы и свойства также наследуются. Например, следующий код динамически создаёт модели для форм, заданных в массиве объекта meta,и добавляет их в список атрибутов модели, определённой this.Class:

          //meta – object that describes a number of forms
          var meta = {
    forms:[
        {
         … //form fields declaration
        }
    ]
}
...
var me = this.Class;
$.each(meta.forms,function(i, form){var metaClass = MVC.String.classize(form.id);
    me.belong_to(metaClass);
    MVC.ReflectionUtils.createJsonPModel(metaClass, {
        domain:me.domain,
        meta:form
       }, {});
    me.add_attribute(form.id, metaClass);
});
...

где meta – некий объект, содержащий массив других объектов. Для итерирования по элементам массива meta.forms используется определённая в jQuery функция .each(index, value)[jquery]. MVC.ReflectionUtils – плагин JavaScriptMVC для динамического создания модели, разработанный автором. Таким образом, можно реализовать, например, динамический интерфейс, если для каждого пользователя запрашивать с сервера его набор предпочтений в виде метаописания. Применительно к приведенному выше примеру, пользователь может определять набор полей в формах, которые он хочет хранить.

Представление

Представление в JavaScriptMVC – это файл с расширением ejs. Этот файл содержит html код с вкраплённым в него JavaScript. JavaScript-код заключается между тегами <% foo() %> и <%= foo() %>. Во втором случае возвращаемое значение функции будет помещено на страницу.

Ниже показан пример представления для модели Address, в файле {JMVC_ROOT}/views/Addresses/create.ejs:

<div class="<%= data.Class.className%>">
    <label>
        <%= data.name %>
        <input type="text"value="(<%= data.phone.area%>)<%= data.phone.number %>" readonly>
    </label>
</div>

Обратиться к представлению в коде можно двумя способами: через конструктор и через API контроллера.

Обращение через конструктор осуществляется следующим образом:

          new View({url:'views/Addresses/create.ejs'}).render({data: {name:”John”}});

Функция .render возвращает html-код. Параметр data – объект JavaScript, к полям которого можно обращаться из кода представления. Например, к полю name можно обратиться из ejs так: <%= data.name %>.

Обращение через API контроллера будет рассмотренно в следующем разделе.

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

Контроллер

Контроллер можно создать при помощи утилиты js: js jmvc/generate/controller [NAME]. Ниже результат работы утилиты:

js jmvc/generate/controller Addresses -> {JMVC_ROOT}/controllers/Addresses_controller.js:

AddressesController = MVC.Controller.extend('Addresses',
/* @Static */
{},
/* @Prototype */
{
});

Вновь созданный контроллер необходимо «включить» в главном скрипте приложения, {JMVC_ROOT}/apps/AddressBook.js:

...

include(function(){ //runs after prior includes are loaded
include.models('Address');
include.controllers('Addresses');
include.views();
});
...

Утилита создаёт также «рыбу» функционального теста для контроллера: {JMVC_ROOT}/test/functional/[NAME]_controller_test.js. Подробнее про автотесты будет сказано ниже.

Очень важно понять то, как контроллер привязывается к элементам на странице. Ниже приведён перевод из автогенерируемой при компрессии документации ({JMVC_ROOT}/docs/classes/MVC.Controller.html):

«Имя контроллера определяет область DOM, к которой он привязан. В зависимости от формы имени – множественное (singular), единственное (plural) или ‘main’ – изменяются элементы в DOM, события которых будут обрабатываться данным контроллером.

Singular-контроллеры реагируют на события элемента DOM с аттрибутом id, равным имени контроллера:

          //matches <div id="file_manager"></div>
FileManagerController = MVC.Controller.extend('file_manager')

Plural-контроллеры реагируют на события элементов DOM, аттрибут class которых равен имени контроллера в единственном числе:

          //matches <div class="task"></div>
 TasksController = MVC.Controller.extend('tasks')

Если необходимо отреагировать на события элемента с атрибутом id в начале имени события, следует приписать символ ‘#’. Например:

TasksController = MVC.Controller.extend('tasks',{
     click : function(){ .. }     //matches <div class="task"></div>"# click" : function(){ .. } //matches <div id="tasks"></div>
 })

Main-контроллеры – это контролеры с именем ‘main’, и они могут отвечать на события всего DOM».

СОВЕТ

В главном контроллере приложения удобно определить событие ‘load’, которое, в некотором приближении, является тем же самым что и body.onLoad. Но, в отличие от body.onLoad, гарантируется, что это событие будет возбуждено только после полной инициализации JavaScriptMVC.

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

Первая группа довольно подробно рассматривается в документации фреймворка. Опустим их рассмотрение в данной статье.

А вот на второй группе стоит особо сконцентрировать внимание. В JavaScriptMVC реализован механизм оповещения OpenAjax. Когда модель изменяет своё состояние, например, создаётся новый экземпляр, она публикует соответствующее событие следующим образом:

OpenAjax.hub.publish(this.className + "." + event, data) 

Здесь приведён код реализации статического метода .publish(event, data), где event – строковое обозначение события; data – экземпляр модели.

Подписка осуществляется следующим образом: “[MODEL_NAME].[EVENT_NAME] subscribe“, например:

AddressesController = MVC.Controller.extend('Addresses',
/* @Static */
{},
/* @Prototype */
{
    “Address.create subscribe”:function(params){        this.data = params.data;// can display with <%= data %>        this.render({action:’create’});    // renders with views/Addresses/create.ejs    }
});

Вышеприведённый пример также интересен тем, что он демонстрирует работу API контроллера для отрисовки результатов обработки события. В частности, в данной функции будет отрисован шаблон представления в файле {JMVC_ROOT}/views/Addresses/create.ejs, в качестве данных в него будет передан только что созданный экземпляр модели Address. Подробнее с API .render можно ознакомиться в автодокументации JavaScriptMVC – {JMVC_ROOT}/docs/classes/MVC.Controller.html

Тестирование, сжатие, эксплуатация

В JavaScriptMVC заложена поддержка как функционального тестирования, так и юнит-тестирования. Для перехода в режим тестирования необходимо назначить параметру загрузки приложения значение ‘test’:

<script language="javascript"type="text/javascript"src="../../jmvc/include.js?AddressBook,test"></script>

Теперь при запуске приложения будет дополнительно открываться окно консоли тестирования. В этой консоли пользователь может запускать разные тесты.

Юнит-тесты также можно запускать из командной строки: js apps/{app_name}/run_unit.js

Ниже приведён фрагмент ant-скрипта, запускающий юнит-тестирование из командной строки на платформе Windows:

<target name="unit-test">
    <echo>Testing javascript...</echo>
    <echo>Executing: js.bat apps/${appName}/run_unit.js</echo>
    <exec dir="${src}"executable="cmd">
        <arg line="/c"/>
        <arg line="js.bat"/>
        <arg line="apps/${appName}/run_unit.js"/>
    </exec>
</target>

Не будем подробно останавливаться на рассмотрении процесса разработки тестов, это подробно описано в документации JavaScriptMVC. Однако стоит уделить особое внимание использованию плагина fixture. Этот плагин иммитирует ответ от сервера, тем самым помогая полностью отделить разработку серверной части от клиентской. Для активации плагина необходимо его подключить в файле {JMVC_ROOT}/apps/{app_name}/test.js:

          include.plugins(
          
          
            'io/jsonp/fixtures'
          
          );

include.unit_tests();
include.functional_tests();

Новые fixture можно создавать при помощи утилиты js: js jmvc/generate/fixture [METHOD] [URL] [NUMBER]. При этом новые fixture будут появляться в папке {JMVC_ROOT}/test/fixtures.

ПРЕДУПРЕЖДЕНИЕ

В версии JavaScriptMVC 1.5 fixture реализованны только для Ajax-запросов. Указанный в примере плагин был разработан автором.

Компрессия приложения производится запуском утилиты js.bat из командной строки или из скрипта автоматической сборки: js apps/${appName}/compress.js. Результатом работы утилиты является файл production.js, который помещается в папку приложения JavaScriptMVC: {JMVC_ROOT}/apps/{app_name}.

СОВЕТ

Во время компрессии также генерируется документация на манер JavaDoc. Документация помещается в папку {JMVC_ROOT}/docs.

Для запуска приложения в рабочем режиме достаточно изменить параметр загрузки приложения в JavaScriptMVC на «production». Применительно к нашему приложению:

<script language="javascript"type="text/javascript"src="../../jmvc/include.js?AddressBook,production"></script>

Фактически, для работы клиентской части javascript в рабочем окружении необходимы только два файла {JMVC_ROOT}/jmvc/include.js и production.js. Стартовую страницу приложения можно создавать динамически. Естественно, также понадобятся стили и другие ресурсы, такие как картинки и прочее. Всё это можно поместить в папку {ROOT}/webapp/client, собрать war и разместить на сервере. Собственно, пришло время рассмотреть процесс разработки серверной части.

Разработка серверной части

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

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

Jsonp-модели клиентской части приложения поддерживают следующие методы обращения к серверу: .create и .find_all, .update и .delete. Соответственно, на сервере нам понадобится интерфейс, реализации которого будут обрабатывать данные запросы:

        public
        interface JsonpRequestHandler<T> {
    T doCreate(HttpRequest req) throws InternalProcessRequestException;
    T[] doFindAll(HttpRequest req) throws InternalProcessRequestException;
    T doUpdate(HttpRequest req) throws InternalProcessRequestException;
    void doDelete(HttpRequest req) throws InternalProcessRequestException;
}

Для обработки запросов определим базовый абстрактный класс JsonpBaseServlet. Основными методом этого класса будет .doGet(HttpRequest, HttpResponse).

Мы не переопределяем метод .doPost класса HttpServlet, поскольку все jsonp-запросы реализуются как GET-запросы.

Реализация метода .doGet является вполне прямолинейной в смысле логики работы: возьми запрос, извлеки параметр callback, делегируй обработку запроса в подкласс (реализующий .doCreate, .doFindAll и т.д.), результат работы транслируй в json, запиши в отклик строку вида callback + “(” + json + “)”. Код реализации метода приведён ниже:

        public abstract class JsonpBaseServlet<T> extends HttpServlet implements JsonpRequestHandler<T>{

public static final String CREATE = “create”;
public static final String FIND_ALL = “find_all”;
...

protected final void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
{
    //extract callback
    String callback = req.getParameter("callback");
    if (callback == null) {
        thrownew ServletException(new IllegalArgumentException("callback parameter is not specified."));
    }

    //extract action
    String action = req.getParameter("action");
    if (action == null) {
        thrownew ServletException(new IllegalArgumentException("action parameter is not specified."));
    }

    resp.setContentType("application/json");

    Object o = null;
    try {
        //delegate execution to subclassif(CREATE.equals(action)){
            o = doCreate(req);
        } elseif(FIND_ALL.equals(action)){
            o = doFindAll(req);
        }
        ...
        } else {
            throw new ServletException(new IllegalStateException("This should never happen."));
        }
    } catch (InternalProcessRequestException e) {
        thrownew ServletException(e);
    }

    String jsonData = getJsonData(o);

    //write output
    PrintWriter respWriter = resp.getWriter();
    respWriter.write(callback);
    respWriter.write("(");
    respWriter.write(jsonData);
    respWriter.write(");"); 
}
...

public abstract T doCreate(HttpServletRequest req) throws InternalProcessRequestException;
    
public abstract T[] doFindAll(HttpServletRequest req) throws InternalProcessRequestException;
...    

Реализация конкретного подкласса не представляет особого интереса, всё, что она делает – создаёт в .doCreate экземпляр Address (серверное представление клиентской модели Address) и помещает его в хранилище; в .doFindAll из хранилища берутся все экземпляры Address, ассоцированные с текущим пользователем, и аналогично для остальных методов.

В качестве улучшения архитектуры можно, например, снабдить класс Address аннотациями и реализовать фабрику, которая через reflection будет создавать его экземпляр на основе данных, присланных клиентом. Разместив такой код в базовом классе, можно изменить сигнатуру абстрактного метода .doCreate:

        public class JsonpBaseServletImpl<T> extends JsonpBaseServlet<T> {
    …
    public abstract T doCreate(T instance) 
      throws InternalProcessRequestException;

}

Как было сказано ранее, кластеризация TomCat достигается за счёт реплицирования объекта HttpSession, ассоциированного с пользователем, между узлами кластера. Тем не менее, при написании серверной части важно помнить, что сервлеты будут исполняться на кластере, поэтому стоит избегать запоминания состояния в любой форме.

В реализации JavaScriptMVC используется соглашение, что модель обращается на сервере к сервлету по своему имени во множественном числе. Например, модель Address будет использовать следующий URL в своих jsonp-запросах: http://my-server.com/Addresses.json. Это соглашение стоит иметь в виду при определении отображения сервлетов в web.xml.

С другой стороны, такое поведение модели в JavaScriptMVC можно переопределить, задав статические свойства create_url, find_url итд. Таким образом, можно адресовать все запросы моделей к одному сервлету – RequestDispatcher’у.

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

В случае рассматриваемого приложения такой интерфейс имеет простейший вид:

        public interface Storage {
    void <T> save(T obj, String user, Context ctx) throws StorageException;
    List<T> load(String user, Context ctx) throws StorageException;
}  

где ctx – контекст, содержащий данные для инициализации конкретного хранилища (если она требуется). Например, bootstrapUrl необходим для создания SocketStoreClientFactory, см. документацию к Voldemort.

Работа с Voldemort ещё удобна тем, что он реализует возможность запуска его в “embedded”-режиме, что существенно упрощает процесс написания серверных тестов к приложению.

Заключение

В статье рассмотрены некоторые моменты реализации распределённого Web-приложения. Основной упор сделан на то, что либо плохо освещено в официальной документации соответствующих продуктов, либо является собственными наработками автора. Большая часть описания реализации посвящена разработке клиентской части на базе JavaScriptMVC 1.5 [jsMVC15].

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

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

Список литературы


    Сообщений 9    Оценка 1 [+0/-1]         Оценить