JS модуль для Java разработчиков

от автора

Во всем мире объем используемого JS кода в приложениях растет очень сильно, что уже неоднократно подчеркивалось, посмотреть картинки на эту тему можно например тут или тут. Соответственно с ростом количества кода возникает необходимость структурирования данных, управления зависимостями и проч., которые на данный момент решает целый букет фрэймворков, например RequireJS в композиции с Backbone. С другой стороны в мире Java для управления зависимостями и контроля процесса сборки проекта используется Maven, который отлично справляется с задачей разделения больших проектов на модули, запуска тестов в нужное время и т.д. У некоторых разработчиков, уже давно использующих Maven для сборки проекта, может возникнуть желание вынести свой отлично структурированый JS код в отдельный модуль, тестировать его во время сборки и совершать с ним все операции, которые позволяют делать плагины, о чем и пойдет речь.

Постановка задачи и выбор фрэймворков

Задача: создать Maven проект с управлением зависимостями, содержащий структурированый JS код и статичную разметку, шаблонизатором, возможностью тестировать части кода по отдельности. Основная идея создания такого модуля заключается в том, что для предоставления статичных html и JS файлов пользователю предпочтительно использовать nginx или apache http server, которые работают быстрее практически любого java веб контейнера или сервера приложений. Сделать автоматическое копирование ресурсов в нужные папки после сборки не составит труда, но нужно исключить из модуля java класс файлы, что в случае с «одностраничными» сайтами, использующими REST сервисы, не составит труда.

Изучив многообразие доступных решений, был выбран следующий набор, который удовлетворяет нашим требованиям:

Подробное описание каждого из этих фрэймворков вам придется прочитать самим, а мы начнем с создания 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *