Сборка фронта на nodejs
На работе появилась задачка, собрать build систему фронта для одного проекта. На самом проекте уже использовался gulp с несколькими плагинами для облегчения повседневной жизни. Как таковой фронт дев версия от прод версии ничем не отличалась, все console.log и комментарии в коде с FIXME и TODO оставались на месте) про минификацию, обфускацию и т.п. речи небыло. Сам проект написан на symfony в качестве билд системы используется capifony, система Assets самой symfony не используется в силу наличия gulp’a, а также у проекта две веб морды, мобильная и настольная + необходимо поддерживать stage и production режимы.
Первое, с чем стоит определиться, так это с набором инструментов и что необходимо сделать. На самом проекте используются следующие типичные фронтенд принадлежности:
- JavaScript — будем транспайлить из ES6 в ES5. На дев версии будем держать ES5 в beautify виде с дебаг сообщениями, на проде будем минифицировать, убирать комментарии и дебаг и применим uglify. Различные плагины просто сожмем.
- SASS — собираем для дева compact для прода compress версии.
- Растровые иконки — собираем в спрайт на деве и проде.
- SVG иконки — собираем шрифт и sass файл.
- Контент изображения и баннеры — обработаем pngquant для облегчения.
Теперь про запуск билда и интеграцию с уже существующими технологиями. Фронтенд разработчик не участвует в момент деплоя проекта и бекенд разработчику не нужно знать о всей тонкости сборки, ему надо просто запустить сказанную фронтом команду. Значит в нашем случае, это будет простейший bash скрипт, который обновит зависимости npm и в зависимости от параметра запустит nodejs с нужными параметрами.
#!/bin/bash #Проверим наличие npm в системе на всякий случай if ! type npm > /dev/null 2>&1; then echo "Node and npm must be installed" exit 1 fi #Обновим зависимости echo "Update NPM packages" npm install #Выходит 4 версии. Мобильный прод и дев и настольный. case $1 in #Desktop production prod) node build.js desktop prod ;; #Desktop development dev) node build.js desktop dev ;; #Mobile production mprod) node build.js mobile prod ;; #Mobile dev mdev) node build.js mobile dev ;; #Default exit *) echo "Undefined build mode" exit 1 esac
Суть и скрипт очень просты. В целом можно и саму ноду было запускать с параметрами, но мы же об этом не собираемся толковать администратору или бекенд разработчику. Теперь переходим к самой nodejs. Что нам нужно? Иметь возможность выполнять две разные сборки (мобильную и настольную) в два разных режима (dev и production). А также сохранить возможность использовать gulp watcher на проекте для повседневной dev разработки.
//Set build mode const buildMode = process.argv[3] || 'dev', buildType = process.argv[2] || 'desktop', gutil = require('gulp-util'), build = require('build.'+buildType); gutil.log('Node build: ', buildType, buildMode, process.cwd()); if(buildMode === 'watch') { //Just watch JS and CSS files in watch mode build.watch(); } else { //Start build build(buildMode); }
Что мы делаем? мы получаем из аргументов buildMode и buildType и подключаем в зависимости от buildType скрипт сборки который в свою очередь уже будет отвечать, за конкретную сборку необходимого нам билда. И в зависимости от параметра buildMode запускаем деплой или ватчер. Я рассмотрю лишь 1 вариант сборки настольной версии, она не сильно отличается от мобильной.
//.............. function deploy (mode) { //Require build config to global for gulp tasks global.buildConf = require('./desktop.'+mode+'.conf.js'); const gulpTasks = require('./gulp.desktop.tasks.js'); gutil.log('Start build: ' + mode); switch(mode) { case 'dev': devDeploy(); break; case 'prod': prodDeploy(); break; default: gutil.log('Undefined build mode'); } } module.exports = deploy; //.............
Первое это сам запуск функции билда. Что есть билд? Это последовательное выполнение gulp задач. В определенную директорию с определенными настройками. Директории и настройки с которыми должны работать задачи мы соответственно выносим в отдельный файл.
//На каждую задачу держим свой объект с настройками. module.exports = { copyAssets: { imagesSrc: './app/Resources/frontend/desktop/images/**/*.*', imagesDist: './web/dev/desktop/images', jsLibs: './app/Resources/frontend/desktop/js/lib/*.js', jsDist: './web/dev/desktop/js/lib' }, clean: { cleanPath: './web/dev/desktop/*' }, sass: { src: './app/Resources/frontend/desktop/sass/**/*.scss', dist: './web/dev/desktop/css', options: { outputStyle: 'compact' }, prefixer: { browsers: ['last 2 version', 'safari 5', 'ie 9'], cascade: false } }, sprite: { src: './app/Resources/frontend/desktop/images/sprite/*.*', imgDist: './web/dev/desktop/images', cssDist: './app/Resources/frontend/desktop/sass', spritesmith: { imgName: 'spritemap.png', cssName: '_sprite.scss', cssFormat: 'scss', padding: 10, algorithmOpts: { sort: false }, cssTemplate: './app/Resources/frontend/desktop/sass/sprite.tmp', imgPath: '../images/spritemap.png' } }, //..............
Т.к. набор задач один, разные только параметры. Держим 1 файл с набором gulp команд. Пример gulp файла…
const gulp = require('gulp'), iconfont = require('gulp-iconfont'), consolidate = require('gulp-consolidate'), runTimestamp = Math.round(Date.now() / 1000), sass = require('gulp-sass'), //............. //Development watcher gulp.task('watch', () => { gutil.log('Start wathing CSS and JS'); gulp.watch(conf.sass.src, ['sass']); gulp.watch(conf.js.src, ['js']); }); //CLEAR gulp.task('clean', () => { gutil.log('Clean dev path'); return gulp.src(conf.clean.cleanPath, {read: false}).pipe(clean()); }); //SASS gulp.task('sass', () => { gutil.log('Make sass'); return gulp.src(conf.sass.src) .pipe(sass(conf.sass.options).on('error', logErr)) .pipe(autoprefixer(conf.sass.prefixer).on('error', logErr)) .pipe(gulp.dest(conf.sass.dist)); }); //...........
Какие задачи и с какими параметрами будем вызвать, мы выяснили, осталось их вызвать. Вернемся к нашему build.desktop.js. В gulp’e есть несколько способов запускать задачи синхронно и устанавливать между ними зависимости. Я же хочу сохранить возможность использовать задачи раздельно друг от друга и для решения пробелмы с асинхронностью задач я воспользусь промисами. Каждую задачу мы будем вызвать по имени навешивать на нее callBack и в нем выполнять resolve для промиса. В конечном итоге мы получим лаконичную цепочку вызовов команд + самый гибкий контроль над их последовательностью и дальнейшему расширению.
Напишем наш promise task wrapper
function promiseTask(name) { return new Promise(function (resolve, reject) { gulp.start(name, (error) => { if(error === null) { gutil.log('Task done'); resolve(true); } else { reject(error); } }) }); }
Теперь осталось написать саму функцию билда)
//Production deploy function prodDeploy() { promiseTask('clean') .then(() =>{ return promiseTask('copy-assets') }) .then(() => { return promiseTask('minimize-images') }) .then(() =>{ return promiseTask('svg') }) .then(() =>{ return promiseTask('sprite') }) .then(() =>{ return promiseTask('js') }) .then(() =>{ return promiseTask('uglify-js') }) .then(() =>{ return promiseTask('sass') }) .then(() => { gutil.log('Build complete'); }) .catch((error) => { gutil.log('Build failed'); gutil.log(error); }); }
Таким вот нехитрым способом начали собирать фронт на проекте) буду рад критике и конструктивным дополнениям)