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

в 13:31, , рубрики: java, javascript, JS, maven, Веб-разработка, метки:

Во всем мире объем используемого 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, в котором будет запущен тестируемый код, не поддерживает ее.

Ссылки

Исходники и заготовку проекта можно взять тут.

В процессе работы были использованы следующие материалы:

Автор: aakhmerov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js