Pull to refresh

Gulp.watch: ловим ошибки правильно

Reading time4 min
Views46K
Во всех современных системах сборки фронтенда есть режим 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-сервере во время билда.
Tags:
Hubs:
Total votes 17: ↑13 and ↓4+9
Comments14

Articles