Постановка задачи и выбор фрэймворков
Задача: создать Maven проект с управлением зависимостями, содержащий структурированый JS код и статичную разметку, шаблонизатором, возможностью тестировать части кода по отдельности. Основная идея создания такого модуля заключается в том, что для предоставления статичных html и JS файлов пользователю предпочтительно использовать nginx или apache http server, которые работают быстрее практически любого java веб контейнера или сервера приложений. Сделать автоматическое копирование ресурсов в нужные папки после сборки не составит труда, но нужно исключить из модуля java класс файлы, что в случае с «одностраничными» сайтами, использующими REST сервисы, не составит труда.
Изучив многообразие доступных решений, был выбран следующий набор, который удовлетворяет нашим требованиям:
- Maven
- Backbone — структурированый JS код
- RequireJS — управление зависимостями
- Handlebars — шаблонизатор
- Jasmine — возможность тестировать
- Jasmine Maven plugin — возможность запускать Jasmine тесты
Подробное описание каждого из этих фрэймворков вам придется прочитать самим, а мы начнем с создания Maven проекта.
Maven проект
Maven диктует нам правила описания проекта и структуру директорий, в которых располагаются исходники нашего приложения. Для создания проекта нам нужно создать pom.xml файл и добавить в него название проекта, версию и прочую стандартную информацию. Пакетирование выбираем war, потому что это веб часть нашего приложения.
Помимо базовой информации о проекте в build секцию нужно добавить объявление ряда плагинов:
- Для запуска тестов в соответствующую фазу сборки, собственно сам maven-jasmine-plugin, с настройками для управления зависимостями при помощи RequireJS
<plugin> <groupId>com.github.searls</groupId> <artifactId>jasmine-maven-plugin</artifactId> <version>1.2.0.0</version> <extensions>true</extensions> <executions> <execution> <goals> <goal>test</goal> </goals> </execution> </executions> <configuration> <jsSrcDir>${project.basedir}/src/main/webapp/js</jsSrcDir> <jsTestSrcDir>${project.basedir}/src/test/js</jsTestSrcDir> <browserVersion>FIREFOX_3</browserVersion> <!--use require js in specs--> <specRunnerTemplate>REQUIRE_JS</specRunnerTemplate> <preloadSources> <source>libs/jasmine/jasmine-jquery-1.3.1.js</source> </preloadSources> <!--customize path to require.js--> <scriptLoaderPath>libs/require/require.js</scriptLoaderPath> </configuration> </plugin> - maven-war-plugin для обеспечения успешной сборки в случае, когда в проекте нет web.xml файла — стандартного обязательного файла описания java веб модулей
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.1</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> - maven-resources-plugin в для обеспечения доступа Jasmine тестов ко всем ресурсам приложения
<plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-js-files</id> <phase>generate-test-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/jasmine</outputDirectory> <resources> <resource> <directory>src/main/webapp</directory> <filtering>false</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin>
После описания проекта нужно создать желаемую структуру директорий. Получившаяся у меня заготовка проекта в результате обладает следующей структурой:
. ├── README.md ├── pom.xml //Maven описание проекта └── src //Корневой каталог (структура Maven) ├── main //Исходники приложения (структура Maven) │ └── webapp //Исходники веб составляющей (структура Maven) │ ├── css │ │ ├── bootstrap.css │ │ ├── style.css │ │ └── styles.css │ ├── imgs │ │ └── 334.gif │ ├── index.html //Индексная страница (одностраничный сайтик) │ ├── js │ │ ├── app.js //инициализация Backbone роутера │ │ ├── libs //библиотеки │ │ │ ├── backbone │ │ │ │ └── backbone-min.js │ │ │ ├── handlebars │ │ │ │ └── handlebars.js │ │ │ ├── jasmine │ │ │ │ └── jasmine-jquery-1.3.1.js │ │ │ ├── jquery │ │ │ │ ├── jquery-min.js │ │ │ │ └── jquery-serialize.js │ │ │ ├── require │ │ │ │ ├── require.js │ │ │ │ └── text.js │ │ │ └── underscore │ │ │ └── underscore-min.js │ │ ├── main.js //Входной файл JS - настройка RequireJS и вызов app.js │ │ ├── router.js //глобальный роутер │ │ └── views //Backbone View сабклассы │ │ └── layout │ │ ├── EmptyContent.js │ │ ├── EmptyFooter.js │ │ ├── NavigationHeader.js │ │ └── PageLayoutView.js │ └── templates //статичные html шаблоны │ └── layout │ ├── emptyContentTemplate.html │ ├── footerTemplate.html │ ├── navigationTemplate.html │ └── simpleTemplate.html └── test // исходники тестов (структура Maven) └── js //Jasmine тесты └── layout └── AboutLayout.js
В приведенной структуре первоначально загружается файл main.js, который объявлен в качестве единственного загружемого скрипта в index.html. Данный скрипт осуществляет инициализацию приложения, которая начинается с Backbone роутера, определяющего компоненты, загружаемые приложением при переходе по различным ссылкам приложения.
Backbone + RequireJS компоненты
Итак, основные компоненты, которыми манипулирует Backbone это объекты, расширяющие View, Model и Collection. Предполагается, что каждый такой набор мы можем сложить в отдельную папку и разбить по подпапкам на основе определенной логики, например, по страницам, в которых они используются. После этого останется только правильно подключать зависимости между компонентами, что в нашем случае будет выглядеть так:
//RequireJS объявление зависимостей define([ 'jquery', 'underscore', 'backbone', //статичный html темплэйт для handlebars 'text!templates/layout/emptyContentTemplate.html', //хак для корректной загрузки Handlebars 'handlebars' ], function($, _, Backbone,emptyContentTemplate){ var EmptyContent = Backbone.View.extend({ }); return EmptyContent;
Templates and Layouts
Как было видно в предыдущем снипете статичная .html разметка, используемая компонентом в качестве основы для Handlebars шаблона, передается как одна из зависимостей при помощи RequireJS. Разметка содержит вкрапления синтаксиса, специфичного для шаблонов, и выглядит примерно так:
<div class="item"> <a href="#/description?id={{id}}">{{title}}</a> </div>
Данный шаблон будет преобразован в полноценную разметку в процессе рендеринга, для чего ему необходимо передать объект содаржащий, значения параметров id и title.
Так как понятия Layout ни один из присутствующих фрэймворков не предоставляет, мы введем свое и назовем его страницей, что в сущности своей будет объектом, расширяющим класс View и содержащим композицию нескольких других View. В методе инициализации данного компонента нужно будет проверить входные параметры, и, если какое-либо из аггрегируемых представлений оверрайдится, использовать экземпляр передаваемого в качестве параметра класса, а не дефолтного.
define([ 'jquery', 'underscore', 'backbone', 'views/layout/NavigationHeader', 'views/layout/EmptyContent', 'views/layout/EmptyFooter', 'text!templates/layout/simpleTemplate.html' , 'handlebars' ], function($, _, Backbone,NavigationHeader,EmptyContent,EmptyFooter,simpleTemplate){ var PageLayoutView = Backbone.View.extend({ template : Handlebars.compile(simpleTemplate), //defaults to NavigationHeader view function headerContent : NavigationHeader, //defaults to EmptyContent view function mainContent : EmptyContent, //defaults to EmptyFooter view function footerContent : EmptyFooter, initialize : function(options) { //instantiate appropriate views based on component functions if (options.mainContent != undefined && options.mainContent != null) { this.mainContent = options.mainContent; } if (options.headerContent != undefined && options.headerContent != null) { this.headerContent = options.headerContent; } if (options.footerContent != undefined && options.footerContent != null) { this.footerContent = options.footerContent; } }, render: function(){ //compile handlebars template with appropriate markup of components var html = this.template(); //append appropriate content to root element right away after compilation $(this.el).html(html); this.headerView = new this.headerContent({el : '#header'}); this.mainView = new this.mainContent({el : '#mian'}); this.footerView = new this.footerContent({el : '#footer'}); this.headerView.render(); this.mainView.render(); this.footerView.render(); return this; } }); return PageLayoutView; });
Тестирование
Тестирование прдлагается осуществлять при помощи Jasmine и соотвтетсвующего плагина. Данный фрэймворк позволяет писать тесты, которые выполняются во время каждой сборки проекта, также есть возможность выполнить цель плагина bdd, что запустит Jetty и позволит вам открыть в браузере страничку с отчетом и прогонять тесты каждый раз при обновлении страницы без полной пересборки проекта. Данный способ очень удобен во время разработки, особенно если вы пишете тесты до кода.
Единственное, что мне пришлось изменить в стандартном описании сценария — это добавление заглушки на консоль, ибо HtmlUnit, в котором будет запущен тестируемый код, не поддерживает ее.
Ссылки
Исходники и заготовку проекта можно взять тут.
В процессе работы были использованы следующие материалы:
ссылка на оригинал статьи http://habrahabr.ru/post/165085/
Добавить комментарий