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-сервере во время билда.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 14
    • +1
      Вы не могли бы выложить это в npm? Чтобы не приходилось копипастить функцию из проекта в проект.
      • 0
        Пока я не уверен, что такая волшебная функция подойдет всем. Например, можно вспользоваться end-of-stream, а не просто слушать событие «end».

        Пока это просто паттерн работы с ошибками в pipe. Когда решение дозреет до настоящего модуля – конечно опубликую
      • +2
        Так в plumber же можно передать функцию-обработчик ошибки…
        • +2
          И что нужно написать в обработчике ошибки, чтобы gulp узнал, что task провалился?
          • 0
            Прошу прощения, неверно понял суть вопроса.
        • 0
          Что-то не понял, а зачем watch при сборке на CI сервере?
          • 0
            watch на локальной машине. Но вы же хотите в режиме watch и билда исполнять одни и те же таски?
            • 0
              Да, поэтому когда я использовал 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.
              • 0
                Да, это тоже решение.

                Но во-первых, дополнительное условие – уже не очень здорово.
                Во-вторых 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-переменной
                • 0
                  совсем не согласен. У вас в арсенале есть еще npm-scripts.
                  {
                    "scripts": {
                      "start": "gulp",
                      "build": "NODE_ENV=production gulp build"
                    }
                  }
                  

                  В итоге вы можете сколько угодно менять систему сборки, разработчики вашей команды, которым они чужды могут даже об этом не знать, запуская npm start каждый раз после git pull.
                  • 0
                    На env обычно завязаны условия, сжимать скрипты или нет, уровень логгирования и т.д. Поэтому лучше разделять опции watch/build и minify/не-minify.

                    А вообще, топик совсем не об этом. А о том, что можно обойтись совсем без специального условия для watch, сделать так, чтобы и для dev-демона и для production сборки применялся один и тот же конфиг. Так ловить production-специфичные баги намного проще.
                    • 0
                      На мой взгляд вашу проблему лучше решать с помощью готовых инструментов, обернув plumber, или аналог, в if. А использовать NODE_ENV, или ENABLE_PLUMBER — это уже вам решать.
                • 0
                  А что не так в webpack?
                  • 0
                    Тем что правила описываются внутри объекта, который разработчик строит как угодно, на свое усмотрение.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.