watch
, при котором запускается специальный демон для автоматической пересборки файлов сразу после их сохранения. Также он есть и gulp.js, но с некоторыми особенностями, делающими работу с ним немного сложней. Работа gulp.js основана на обработке файлов как потоков данных (streams). И ошибки в потоках не всегда можно перехватить, а когда ошибка случается, то будет выброшено неперехваченное исключение и процесс завершится.Чтобы этого не происходило, и watcher мог игнорировать отдельные ошибки и запускаться снова и снова при сохранении файлов, в gulp нужно сделать несколько дополнительных настроек, о которых и расскажется в этой статье.
Что происходит?
Для начала разберемся что происходит и почему же watcher иногда падает. При написании плагина к gulp рекомендуется испускать событие
error
при возникновении ошибок во время работы плагина. Но nodejs-потоки, на которых основана система сборки, не позволяют ошибкам оставаться незамеченными. В случае, если на событие error
никто не подписался, выбросится исключение, чтобы сообщение точно достигло пользователя. В результате, при работе с gulp разработчики часто видят такие ошибкиevents.js:72
throw er; // Unhandled 'error' event
При применении Continious-Integration (CI), когда на каждый коммит запускаются автоматические проверки, это может быть и полезным (сборка провалилась, билд не собрался, ответственные получат письма). Но вечно падающий watcher в локальной разработке, который нужно постоянно перезапускать – это большая неприятность.
Что делать?
В интернете можно найти вопросы этой теме как на stackoverflow, так и на тостере. В ответах на вопросы предлагают несколько популярных решений.
Можно подписаться на событие
error
:gulp.task('less', function() {
return gulp.src('less/*.less')
.pipe(less().on('error', gutil.log))
.pipe(gulp.dest('app/css'));
});
Можно подключить плагин gulp-plumber, который не только подпишется на
error
для одного плагина, но и автоматически сделает это и для всех последующих плагинов, подключенных через pipe:gulp.task('less', function() {
return gulp.src('less/*.less')
.pipe(plumber())
.pipe(less())
.pipe(gulp.dest('app/css'));
});
Почему так не надо делать
Несмотря на то, что проблема выглядит как решенная, это не так. Теперь ошибки сборки перехватываются, мы получаем в консоль сообщения об этом, запущенный демон-watcher не падает, но у нас теперь есть новая проблема.
Допустим у нас есть CI-сервер, где идет сборка наших скриптов для выкладки. Чтобы наша сборка работала вместе с watch, мы применили одно из решений выше. Но теперь оказывается, что при ошибках в сборке наш билд все равно отмечается как успешный. Команда gulp всегда завершается с кодом 0. Получается, для CI-сборки нам не нужно проглатывать ошибки. Можно добавлять свой обработчик ошибок только для watch режима, но это усложнит описание сборки и повысит шансы допустить ошибку. К счастью, есть решение, как настроить сборку одинаково, но при этом поддержать работу и в режиме билда и в режиме watch.
Решение
На самом деле gulp пытается слушать ошибки в потоках файлов. Но из-за того, что обычно последним идет вызов
gulp.dest()
, записывающий результат на диск, а ошибки случаются в промежуточных плагинах, то ошибка не достигает конца цепочки, поэтому gulp о ней не узнает. Например рассмотрим такую цепочку:stream1.on('error', function() { console.log('error 1') })
stream2.on('error', function() { console.log('error 2') })
stream3.on('error', function() { console.log('error 3') })
stream1
.pipe(stream2)
.pipe(stream3);
stream1.emit('error');
// в консоли выведется только "error 1"
В отличие от, например, promise, ошибки в потоках не распространяются дальше по цепочке, их нужно перехватывать в каждом потоке отдельно. По этому поводу есть pull-request в io.js, но в нынешних версиях передать ошибку в конец цепочки не получится. Поэтому gulp не может перехватить ошибки промежуточных потоках и нам это нужно делать самостоятельно.
Зато gulp в качестве описания task принимает не только функцию, возвращающую поток, но и обычную callback-style функцию, как и многие API в node.js. В такой функции мы сами будем решать, когда задача завершилось с ошибкой, а когда успешно:
gulp.task('less', function(done) {
gulp.src('less/*.less')
.pipe(less().on('error', function(error) {
// у нас ошибка
done(error);
}))
.pipe(gulp.dest('app/css'))
.on('end', function() {
// у нас все закончилось успешно
done();
});
});
Такое описание сборки выглядит немного больше, потому что мы теперь делаем часть работы за gulp, но теперь мы ее делаем правильно. А если написать себе вспомогательную функцию, вроде такой, то разница будет практически незаметна:
gulp.task('less', wrapPipe(function(success, error) {
return gulp.src('less/*.less')
.pipe(less().on('error', error))
.pipe(gulp.dest('app/css'));
}));
Зато мы получим правильные сообщения в консоли как во время разработки и пересборке при сохранении, так и на CI-сервере во время билда.
Комментарии (14)
hell0w0rd
01.06.2015 21:34Что-то не понял, а зачем watch при сборке на CI сервере?
justboris Автор
01.06.2015 22:24watch на локальной машине. Но вы же хотите в режиме watch и билда исполнять одни и те же таски?
hell0w0rd
01.06.2015 22:56Да, поэтому когда я использовал gulp, делал так:
var gulp = require('gulp'); var _if = require('gulp-if'); var env = process.env.NODE_ENV || 'development'; var production = env === 'production'; gulp.task('less', function () { gulp.src('less/*.less') .pipe(_if(!production, plumber())) .pipe(less()) });
Чего очень сильно не хватает в webpack.justboris Автор
01.06.2015 23:20Да, это тоже решение.
Но во-первых, дополнительное условие – уже не очень здорово.
Во-вторых plumber должен включаться не в зависимости от environment, а от запускаемой задачи. Получается, нужно делать как-то так:
var usePlumber = false; gulp.task('dev', function() { usePlumber = true; gulp.start() }); gulp.task('less', function () { gulp.src('less/*.less') .pipe(_if(usePlumber, plumber())) .pipe(less()) });
Командыgulp dev
иgulp build
(подозреваю, что вы собираете не только стили) задокументировать и объяснить другим участникам команды проще, чем в комбинации с environment-переменнойhell0w0rd
01.06.2015 23:32совсем не согласен. У вас в арсенале есть еще npm-scripts.
{ "scripts": { "start": "gulp", "build": "NODE_ENV=production gulp build" } }
В итоге вы можете сколько угодно менять систему сборки, разработчики вашей команды, которым они чужды могут даже об этом не знать, запускаяnpm start
каждый раз послеgit pull
.justboris Автор
02.06.2015 00:07На env обычно завязаны условия, сжимать скрипты или нет, уровень логгирования и т.д. Поэтому лучше разделять опции watch/build и minify/не-minify.
А вообще, топик совсем не об этом. А о том, что можно обойтись совсем без специального условия для watch, сделать так, чтобы и для dev-демона и для production сборки применялся один и тот же конфиг. Так ловить production-специфичные баги намного проще.hell0w0rd
02.06.2015 00:43На мой взгляд вашу проблему лучше решать с помощью готовых инструментов, обернув plumber, или аналог, в if. А использовать NODE_ENV, или ENABLE_PLUMBER — это уже вам решать.
lolmaus
Вы не могли бы выложить это в npm? Чтобы не приходилось копипастить функцию из проекта в проект.
justboris Автор
Пока я не уверен, что такая волшебная функция подойдет всем. Например, можно вспользоваться end-of-stream, а не просто слушать событие «end».
Пока это просто паттерн работы с ошибками в pipe. Когда решение дозреет до настоящего модуля – конечно опубликую