Сборка фронта на 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);
});
}
Таким вот нехитрым способом начали собирать фронт на проекте) буду рад критике и конструктивным дополнениям)