Gulp.watch: ловим ошибки правильно
Во всех современных системах сборки фронтенда есть режим 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-сервере во время билда.