Недавно понадобилось запрототипировать один проект.
Это должно быть приложение с сервером на spring boot и клиентским js application. Поскольку был избыток времени,
я решил поиграться с системами сборки, обеспечив простую и легкую разработку проекта. Для этого пришлось
скомбинировать gradle, npm, bower и gulp.
Шаг 5: Установка gupl. Gulp устанавливается с помощью npm. Для этого нужно создать файл pacakge.json и запустить npm install
Хотелось следующего:
- Не париться с зависимостями как на клиенте, так и на сервере
- Не тащить кучу лишнего в готовый 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
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 по цепочке будет вызывать все задания и в итоге главное задание будет выполнено.
На этом этапе скорее всего возникнет сразу два вопроса:
- Кто знает какие файлы у зависимостей главные?
У каждой зависимости есть файл bower.json, в котором указаны главные файлы в разделе main. В bower.json приложения это можно переопределить.
- Что если в приложении добавили, например, шрифт или еще какой-нибудь ресурс, который ни 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, но еще не уверен, не создаст ли это больше проблем, чем даст выгоды.
и чего вы все thymeleaf лепите ...
ОтветитьУдалитьКурть :) Для запуска gulp из gradle посмотри https://plugins.gradle.org/plugin/com.moowork.gulp
ОтветитьУдалитьПример здесь:
http://broonix-rants.ghost.io/spring-boot-building-bootstrap-with-gulp-2/