Во всем мире объем используемого JS кода в приложениях растет очень сильно, что уже неоднократно подчеркивалось, посмотреть картинки на эту тему можно например тут или тут. Соответственно с ростом количества кода возникает необходимость структурирования данных, управления зависимостями и проч., которые на данный момент решает целый букет фрэймворков, например RequireJS в композиции с Backbone. С другой стороны в мире Java для управления зависимостями и контроля процесса сборки проекта используется Maven, который отлично справляется с задачей разделения больших проектов на модули, запуска тестов в нужное время и т.д. У некоторых разработчиков, уже давно использующих Maven для сборки проекта, может возникнуть желание вынести свой отлично структурированый JS код в отдельный модуль, тестировать его во время сборки и совершать с ним все операции, которые позволяют делать плагины, о чем и пойдет речь.
Постановка задачи и выбор фрэймворков
Задача: создать 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, в котором будет запущен тестируемый код, не поддерживает ее.
Ссылки
Исходники и заготовку проекта можно взять тут.
В процессе работы были использованы следующие материалы:
- searls.github.com/jasmine-maven-plugin/
- pseudobry.com/jasminemavenrequirejscoverage/
- backbonetutorials.com/organizing-backbone-using-modules/
Автор: aakhmerov