5 февр. 2016 г.

Система сборки проекта spring boot используя gradle, gulp, bower и npm

Недавно понадобилось запрототипировать один проект. Это должно быть приложение с сервером на spring boot и клиентским js application. Поскольку был избыток времени, я решил поиграться с системами сборки, обеспечив простую и легкую разработку проекта. Для этого пришлось скомбинировать gradle, npm, bower и gulp.



Хотелось следующего:
  • Не париться с зависимостями как на клиенте, так и на сервере
  • Не тащить кучу лишнего в готовый jar проекта 
  • Быстрый reload изменений как на клиенте, так и на сервере
  • Поддержка IDE
  • Возможность разделить разработку frontend/backend
  • Несложная и прозрачная система сборки
В принципе, по отдельности все давным давно уже придумано. За серверную сборку отвечает gradle, на клиенте npm, bower и gulp. Вопрос только, как это все объединить. Из готового коробочного нашел только http://jhipster.github.io/ , но пробовать не стал. Хотелось прозрачности и гибкости, ну и просто потренироваться самому.


Выше я нарисовал типичное устройство spring boot веб приложения. В каталоге resources есть два подкаталога: templates и static. В первом расположены html страницы, которые являются view для контроллеров. В static расположены статичные ресурсы: css, js, img и другое. Разрабатывать что-то серьезное с такой структурой, на мой взгляд, неудобно. Я сразу же вижу такие минусы:
  • Нужны дополнительные телодвижения для поддержки IDE
  • При создании тут рабочих файлов для разработки они без дополнительной фильтрации от gradle будут попадать в итоговый jar. 
  • Даже после решения двух предыдущих проблем, не уверен, что frontend девелоперу будет просто приятно разрабатывать в таких непонятных ограничениях
Соответственно появляется идея перенести клиентскую разработку в отдельное место, а скрипты сборки - заставить переносить наработки куда надо и как надо. Я сделал это за несколько шагов (ссылка на гитхаб): 

Шаг 1: Установка nodejs, bower, gulp, gradle
NodeJS устанавливается здесь, gradle тут, а bower и gulp могут быть установлены прямо из node js в две строки:

npm isntall bower -g
npm install gulp -g

Шаг 2: Создание простого spring-boot веб приложения. Например, как это рассказывается тут

Шаг 3: Создание инфраструктуры для клиентской разработки. 

Как я уже написал, всю клиентскую разработку я поместил в соседний с main каталог. Структура каталогов изображена выше. Файлы делятся на следующие категории (выделены цветами):

  • Конфигурационный файлы. 
  • Генерируемые файлы не входящие в итоговый build
  • Генерируемые файлы входящие в итоговый build (зависимости)
  • Продукты разработки человека.
Шаг 4: Настройка bower. Для этого нужно сделать следующее:
  • Прописать в .bowerrc директорию для расположения генерируемых файлов
  • Указать в bower.json необходимые свойства проекта и зависимости.
Ниже .bowerrc
{
  "directory": "app/components/"
}
И bower.json. Для теста я указал одну зависимость, которая подтянет за собой еще jquery
{
  "name": "client",
  "description": "",
  "main": "",
  "moduleType": [],
  "license": "MIT",
  "dependencies": {
    "bootstrap": "^3.3.6"
  }
}
Хочу заметить, что добавлять зависимости гораздо удобнее через командную строку: bower -install bootstrap -s . Далее по команде bower install будет создан каталог components, куда bower скачает все зависимости. 


Шаг 5: Установка gupl. Gulp устанавливается с помощью npm. Для этого нужно создать файл pacakge.json и запустить npm install
{
  "devDependencies": {
    "gulp": "^3.9.0",
  }
}
Но удобнее просто из командой строки npm install gulp в домашнем клиентском каталоге. (src/client) Также нужно создать gulpfile.js, в котором будут размещены правила сборки. 



Шаг 6: Создание правил клиентской сборки. Gulp в некотором смысле подобен gradle. В файле gulp.js описываются tasks, которые зависят друг от друга. Таким образом, можно формировать task дерево.
Главную задачу я разделил на следующие подзадачи:
  • Копирование html файлов (copyHtmlFiles)
  • Копирование изображений (copyImgFiles)
  • Копирование пользовательских js скриптов (copyScriptFiles)
  • Копирование пользовательских css файлов (copyStyleFiles)
  • Копирование библиотечных js файлов (copyJsFiles)
  • Копирование библиотечных css файлов (copyCssFiles) 
Поскольку ничего умного система практически не делает, по сути это просто выборочное копирование файлов, то зависимости создаются требованием предварительной очистки каталога назначения и наличия файлов в каталоге, откуда будет произведено копирование. Например, перед тем как скопировать html файлы в каталог назначения (copyHtmlFiles), необходимо его очистить. (cleanHtmlFiles)

Немного сложнее с библиотечными css и js файлами. Они находятся в каталоге components, причем там кроме этого находится много чего другого, что не хочется помещать в итоговый build. Для этого существует инструмент, выделяющий необходимые файлы. Я выделяю главные js и css файлы и копирую в соответствующие директории app/js и app/css (не забывая их чистить). За выделение главных файлов отвечают taskи, начинающиеся с main. 

В итоге получилось такое дерево заданий. Цвет визуализирует удаленность от центра для наглядности. 



Вызов из командой строки gulp copyFles по цепочке будет вызывать все задания и в итоге главное задание будет выполнено.  

На этом этапе скорее всего возникнет сразу два вопроса:

  1. Кто знает какие файлы у зависимостей главные? 

У каждой зависимости есть файл bower.json, в котором указаны главные файлы в разделе main. В bower.json приложения это можно переопределить.

  1. Что если в приложении добавили, например, шрифт или еще какой-нибудь ресурс, который ни css, ни js?

Нужно просто добавить еще пару тасков на новый каталог.

Чтобы все это заработало, нужно установить несколько gulp плагинов:
npm install gulp-clean main-bower-files  

Итоговый gulpфайл тут

Шаг 7: Live reload. Возможности spring-boot позволяют обновлять статику по Ctrl+F9, таким же образом можно обновлять небольшие изменения кода. Настройка в application.properties spring.thymeleaf.cache=false включит обновление html страниц, они же шаблоны thymleaf.  Соответственно, осталось обеспечить выполнение задания copyFiles при любых изменениях, чтобы своевременно подгружать изменения из зоны разработки клиента (app/client). Для этого можно воспользоваться плагином gulp-watch. 
npm install gulp-watch

gulp.task('watch', function() {
    gulp.watch(['app/scripts/**', 'app/style/**', 'app/components/**', 'app/img/**', 'app/*.html'], ['copyFiles']);
});

Выше объявление gulp task, который мониторит изменения в следующих каталогах и запускает в ответ на них task copyFiles. Этот task ни от кого не зависит и запускается только лишь на время отладки. Остановка выполнения этого задания (прекращение мониторинга) производится вручную.

Шаг 8: Интеграция с gradle. Когда я выполняю checkout проекта, то там не будет генерируемых файлов. Все это необходимо сгенерить самостоятельно. По-сути gradle должен установить npm зависимости, bower зависимости и выполнить скрипт сборки gulp. В командой строке это делается так:
npm install
bower install
gulp copyFiles

Сделать это можно простыми gradle task типа exec. Но есть одна проблема: в windows, чтобы из gradle обратиться в npm, нужно писать не npm, а npm.cmd. Например, вот так в windows из gradle узнается версия: npm.cmd --version

Поэтому пришлось немного доработать exec task у gradle (в build.gradle):
class ExecAdvanced extends Exec {

    void setExecObject(String execObject) {
        if (Os.isFamily(Os.FAMILY_WINDOWS)) {
            setExecutable(execObject + ".cmd")
        } else {
            setExecutable(execObject);
        }
    }
}

Как видно, я просто добавляю .cmd для windows. Остается только создать три последовательных gradle taska в build.gradle
task npm(type: ExecAdvanced) {
    workingDir 'src/client'
    execObject 'npm'
    args "install"
}

task bower(type: ExecAdvanced, dependsOn: npm) {
    workingDir 'src/client'
    execObject 'bower'
    args "install"
}

task gulp(type: ExecAdvanced, dependsOn: bower) {
    workingDir 'src/client'
    execObject 'gulp'
    args "copyFiles"
}
processResources.dependsOn gulp
Последней строкой я привязываю свое дерево заданий к основному дереву gradle. Таким образом, в самом начало сценария сборки выполняется сборка клиентского приложения, которое потом также попадает в итоговый build. 

Все.


Я еще не успел попробовать эту схему в разработке, возможно, что-то можно было сделать по-другому, но все очень гибко, просто и легко меняется. И удобно!

Но есть идея, как сделать еще удобнее. Заметил, что когда в клиентском коде пишешь url какого-нибудь запроса, то IDE не подсказывает его адрес. Хотя теоретически это во многих случаях возможно, если есть серверный код под рукой.  Это можно исправить, генерируя некий файл urlList.js с константами в виде адресов, собранных по проекту с помощью reflection. Имя константы IDE уже подскажет. Я подумываю о том, чтобы реализовать это хотя бы по аннотации @RequestMapping, но еще не уверен, не создаст ли это больше проблем, чем даст выгоды.

2 комментария :

  1. и чего вы все thymeleaf лепите ...

    ОтветитьУдалить
  2. Курть :) Для запуска gulp из gradle посмотри https://plugins.gradle.org/plugin/com.moowork.gulp
    Пример здесь:
    http://broonix-rants.ghost.io/spring-boot-building-bootstrap-with-gulp-2/

    ОтветитьУдалить