Gulp 3 から 4 に変えたら Browser-Sync が動かなくなったので全面的に修正した・変更点をおさらい

久々に Gulp の話。

Node.js v12 にアップデートすると、Gulp v3 で graceful-fs 関連のエラーが出るので、Gulp v4 にアップデートすることにした。メジャーバージョンアップが変わり、色々とお作法が変わっているので、v3 のコードと v4 のコードを比較しながら、変更点をおさらいする。

目次

変更前後のコード全量

今回修正したコードの全量は以下で確認できる。

Gulp v3 版を基に、ほぼほぼ同じ動作をするよう修正している。以降はタスクごとに解説していく。

冒頭

const gulp = require('gulp');
const $ = require('gulp-load-plugins')({
  pattern: ['browser-sync', 'browserify', 'del', 'run-sequence', 'vinyl-buffer', 'vinyl-source-stream'],
  overridePattern: false,  // デフォルトのパターン ('gulp-*', 'gulp.*', '@*/gulp{-,.}*') を残す
  maintainScope: false     // スコープパッケージを階層化しない
});
const gulp = require('gulp');
const $ = require('gulp-load-plugins')({
  pattern: ['browser-sync', 'browserify', 'del', 'vinyl-buffer', 'vinyl-source-stream'],
  overridePattern: false,  // デフォルトのパターン ('gulp-*', 'gulp.*', '@*/gulp{-,.}*') を残す
  maintainScope: false     // スコープパッケージを階層化しない
});

大きな違いはないが、Gulp v4 で以下のパッケージが不要になった。

主にこれらの使い方の解説がメインになる。

HTML のビルド (html-all タスク)

/**
 * テンプレート HTML を適用して全ファイルを出力する
 */
gulp.task('html-all', () => {
  return gulp
    .src('./src/pages/**/*.html')
    .pipe($.templateHtml('./src/templates/template.html'))
    .pipe(gulp.dest('./docs'));
});
/**
 * テンプレート HTML を適用して全ファイルを出力する (追加・変更のみ、削除には非対応)
 */
function htmlAll(done) {
  gulp
    .src('./src/pages/**/*.html')
    .pipe($.templateHtml('./src/templates/template.html'))
    .pipe(gulp.dest('./docs'));
  done();
}
gulp.task('html-all', htmlAll);

Gulp v4 では、生の JavaScript Function を宣言しておくと使い回しが効くので、function hoge() で単体の関数を宣言しておくと良い。ココで作成した htmlAll() 関数も後で使用している。

関数は return gulp... とするのではなく、第1引数に done を取り、done() を呼んで終了することで、タスクの直列実行などをハンドリングできるようになる。この書き方に統一しておこう。

HTML の差分ビルド (html タスク)

/**
 * テンプレート HTML を適用して出力する (差分のみ)
 */
gulp.task('html', () => {
  return gulp
    .src('./src/pages/**/*.html')
    .pipe($.changed('./docs'))
    .pipe($.templateHtml('./src/templates/template.html'))
    .pipe(gulp.dest('./docs'));
});
/**
 * テンプレート HTML を適用して出力する (追加・変更のみ、削除には非対応)
 */
function html(done) {
  gulp
    .src('./src/pages/**/*.html', {
      since: gulp.lastRun(html)
    })
    .pipe($.templateHtml('./src/templates/template.html'))
    .pipe(gulp.dest('./docs'));
  done();
}
gulp.task('html', html);

gulp.src() で指定したファイル群の内、変更があったファイルだけを特定してビルド処理を呼ぶタスク。v3 では gulp-changed パッケージを使って差分特定をしていたが、v4 では gulp.src()since オプションに、gulp.lastRun() 関数を組み合わせることで実現できるようになった。ココで、gulp.lastRun(html) と、自分自身の関数を引数に設定しているので、関数宣言が必須となる。

不思議なのだが、ターミナルで $ npm run gulp html と複数回手動で実行した際も、ファイルの差分が検知できる。どこか隠しファイルでタスクの実行履歴とかを管理しているのだろうか。

SCSS のビルド (css タスク) : 大きな差異なし

/**
 * SCSS をトランスパイルして CSS を出力する
 */
gulp.task('css', () => {
  return gulp
    .src(['./src/styles/index.scss'])  // エントリポイント
    .pipe($.plumber(function(error) {
      return this.emit('end');
    }))
    .pipe(
      $.sass({
        outputStyle: 'compressed'
      })
      .on('error', $.sass.logError)
    )
    .pipe($.cleanCss())              // import('.css') をインライン化して全体を圧縮・ついでに UTF-8 BOM を除去する
    .pipe($.rename('./styles.css'))  // リネームする
    .pipe(gulp.dest('./docs'));      // ./docs/styles.css を出力する
});
/**
 * SCSS をトランスパイルして CSS を出力する
 */
function css(done) {
  gulp
    .src(['./src/styles/index.scss'])  // エントリポイント
    .pipe($.plumber(function(_error) {
      return this.emit('end');
    }))
    .pipe(
      $.sass({
        outputStyle: 'compressed'
      })
      .on('error', $.sass.logError)
    )
    .pipe($.cleanCss())              // import('.css') をインライン化して全体を圧縮・ついでに UTF-8 BOM を除去する
    .pipe($.rename('./styles.css'))  // リネームする
    .pipe(gulp.dest('./docs'));      // ./docs/styles.css を出力する
  done();
}
gulp.task('css', css);

ほぼ違いなし。function に切り出したのと、return gulp... ではなく done() で終了するようにしただけ。

ES2015 のトランスパイル (js タスク) : 大きな差異なし

/**
 * ES2015 をトランスパイルして JS を出力する
 */
gulp.task('js', () => {
  return $.browserify({
    entries: ['./src/scripts/index.js'],  // エントリポイント
    transform: [
      ['babelify', {
        presets: ['@babel/preset-env']
      }]
    ]
  })
    .bundle()  // Do Browserify!
    .on('error', function(error) {
      console.log(`Browserify Error : ${error.message}`);
      this.emit('end');
    })
    .pipe($.vinylSourceStream('scripts.js'))  // Vinyl に変換しリネームする
    .pipe($.vinylBuffer())                    // Uglify できるように変換する
    .pipe($.uglify())                         // Uglify する
    .pipe(gulp.dest('./docs'));               // ./docs/scripts.js を出力する
});
/**
 * ES2015 をトランスパイルして JS を出力する
 */
function js(done) {
  $.browserify({
    entries: ['./src/scripts/index.js'],  // エントリポイント
    transform: [
      ['babelify', {
        presets: ['@babel/preset-env']
      }]
    ]
  })
    .bundle()  // Do Browserify!
    .on('error', function(error) {
      console.log(`Browserify Error : ${error.message}`);
      this.emit('end');
    })
    .pipe($.vinylSourceStream('scripts.js'))  // Vinyl に変換しリネームする
    .pipe($.vinylBuffer())                    // Uglify できるように変換する
    .pipe($.uglify())                         // Uglify する
    .pipe(gulp.dest('./docs'));               // ./docs/scripts.js を出力する
  done();
}
gulp.task('js', js);

コチラも大きな違いなし。

画像ファイルなどのコピー (assets-all タスク) : 大きな差異なし

/**
 * HTML・CSS・JS 以外の画像ファイルなどを全てコピーする
 */
gulp.task('assets-all', () => {
  return gulp
    .src(assetFileNames, { base: 'src/pages' })
    .pipe(gulp.dest('docs'));
});
/**
 * HTML・CSS・JS 以外の画像ファイルなどを全てコピーする (追加・変更のみ、削除には非対応)
 */
function assetsAll(done) {
  gulp
    .src(assetFileNames, { base: 'src/pages' })
    .pipe(gulp.dest('docs'));
  done();
}
gulp.task('assets-all', assetsAll);

コレも大きな差異なし。

画像ファイルなどの差分コピー (assets タスク) : 大きな差異なし

/**
 * HTML・CSS・JS 以外の画像ファイルなどをコピーする (差分のみ)
 */
gulp.task('assets', () => {
  return gulp
    .src(assetFileNames, { base: 'src/pages' })
    .pipe($.changed('./docs'))
    .pipe(gulp.dest('docs'));
});
/**
 * HTML・CSS・JS 以外の画像ファイルなどをコピーする (追加・変更のみ、削除には非対応)
 */
function assets(done) {
  gulp
    .src(assetFileNames, {
      base: 'src/pages',
      since: gulp.lastRun(assets)
    })
    .pipe(gulp.dest('docs'));
  done();
}
gulp.task('assets', assets);

コレも大きな差異なし。gulp.lastRun() を使うように変えた。前回実行時から削除されたファイルの検知は上手くできないので要注意。

全量ビルド (build タスク)

/**
 * docs ディレクトリ配下のファイルを削除する
 */
gulp.task('clean', () => {
  return $.del(['./docs/**/*']);
});

/**
 * 全ファイルをビルドする
 */
gulp.task('build', (callback) => {
  return $.runSequence(
    'clean',
    ['html-all', 'css', 'js', 'assets-all'],
    callback
  );
});
/**
 * docs ディレクトリ配下のファイルを削除する
 */
function clean(done) {
  $.del(['./docs/**/*', './docs/.htaccess']);
  done();
}
gulp.task('clean', clean);

/**
 * 全ファイルをビルドする : コレだけ関数で囲まない
 */
const build = gulp.series(
  clean,
  gulp.parallel(htmlAll, css, js, assetsAll)
);
gulp.task('build', build);

clean タスクは、消したいファイルが上手く消せていなかったための微修正が入っている。それ以外は done() を呼んで終わる形に直しただけ。

メインは build タスク。run-sequence を使っていたところを gulp.series()gulp.parallel() だけで再現するように変更している。関数を直接引数に指定している。どうも定義済の Gulp タスク名を文字列で指定しても大丈夫みたいだが、とりあえずこうしておいた。

gulp.series() で束ねた内容を gulp.task() で定義する際は、gulp.task() の第2引数に直接 gulp.series() を渡してやる必要がある。だから function build() という関数の形式ではなく、const build とあくまで変数として定義しているワケ。コレをミスると何のタスクも実行されないので注意。

ファイルの変更を検知するライブリロード (dev タスク)

/**
 * ライブリロード開発用に Browser-Sync サーバを起動する
 */
gulp.task('browser-sync', () => {
  return $.browserSync.init({
    server: {
      baseDir: "docs",
      index: "index.html"
    }
  });
});

/**
 * リロードする
 */
gulp.task('reload', function () {
  return $.browserSync.reload();
});

/**
 * ファイルを監視してライブリロード開発を行う
 */
gulp.task('dev', ['browser-sync'], function () {
  // src ファイルを監視して処理する
  $.watch('./src/styles/**/*.scss', () => {
    return gulp.start(['css'])
  });
  $.watch('./src/scripts/**/*.js', () => {
    return gulp.start(['js']);
  });
  $.watch('./src/pages/**/*.html', (file) => {
    // ファイルが削除された時は docs ディレクトリからも削除する
    if(file.event === 'unlink') {
      return $.del(file.path.replace(/src\\pages/, 'docs').replace(/src\/pages/, 'docs'));
    }
    return gulp.start(['html']);
  });
  // テンプレート変更時は全ファイルに再適用する
  $.watch('./src/templates/**/*.html', () => {
    return gulp.start(['html-all']);
  });
  $.watch(assetFileNames, (file) => {
    // ファイルが削除された時は docs ディレクトリからも削除する
    if(file.event === 'unlink') {
      return $.del(file.path.replace(/src\\pages/, 'docs').replace(/src\/pages/, 'docs'));
    }
    return gulp.start(['assets']);
  });
  
  // docs ファイルを監視してライブリロードする
  $.watch('./docs/**/*', () => {
    return gulp.start(['reload']);
  });
});
/**
 * ライブリロード開発用に Browser-Sync サーバを起動する
 */
function initBrowserSync(done) {
  $.browserSync.init({
    server: {
      baseDir: 'docs',
      index: 'index.html'
    }
  });
  done();
}

/** 監視する */
function watch() {
  /** リロードする */
  const reload = (done) => {
    $.browserSync.reload();
    done();
  };
  /** ファイルを削除する */
  const removeFile = (path) => {
    $.del(path.replace(/src\\pages/, 'docs').replace(/src\/pages/, 'docs'));
  };
  
  // src ファイルを監視して処理する
  gulp.watch('./src/styles/**/*.scss'   ).on('change', gulp.series(css    , reload));
  gulp.watch('./src/scripts/**/*.js'    ).on('change', gulp.series(js     , reload));
  gulp.watch('./src/templates/**/*.html').on('change', gulp.series(htmlAll, reload));  // テンプレート HTML 変更時は全 HTML ファイルに再適用する
  // 各 HTML
  gulp.watch('./src/pages/**/*.html')
    .on('add'   , gulp.series(html, reload))
    .on('change', gulp.series(html, reload))
    .on('unlink', removeFile);
  // アセットファイル
  gulp.watch(assetFileNames)
    .on('add'   , gulp.series(assets))
    .on('change', gulp.series(assets))
    .on('unlink', removeFile);
}

/**
 * ファイルを監視してライブリロード開発を行う
 */
gulp.task('dev', gulp.series(initBrowserSync, watch));

ココが一番大きな変更だった。

v3 における browser-sync タスクは、v4 では initBrowserSync() 関数として定義し、最後に登場するgulp.task('dev') にて指定している。gulp.series()initBrowserSync()watch() の順に直列実行するよう記述しているので、Browser-Sync が起動してからファイル監視が始まるワケ。

v3 の reload タスクは、v4 では watch() 関数内に記述した。watch() 関数内でしか使用しないので、このような書き方ができる。

v3 の dev タスク内に書いていた一連の処理は、v4 では watch() 関数に切り出している。gulp-watch パッケージを使っていたところ、Gulp 本体に組み込みの gulp.watch() 関数を使うよう変更している。

gulp.watch() 関数の実体は chokidar というファイルの変更監視ライブラリで、.on('change') (変更) や .on('add') (追加)、.on('unlink') (削除) といった指定は chokidar と同じ。

監視するファイルに合わせて、追加・変更時はビルドタスクを呼び、その後ブラウザをリロードするよう、gulp.series() で指定している。v4 のコードの方が、何をしているのか推測しやすく・読みやすくなったと思う。

gulp.watch().on('unlink') 時に対象ファイルを削除する処理を、removeFile() 関数に切り出した。.on() の第2引数に指定したコールバック関数には、仮引数 path が渡るので、削除されたファイルのパスが分かる。コレを使用して削除処理を行っている。

以上

というワケで、Gulp v3 から Gulp v4 へのマイグレーションが完了した。

コレまで使用していた外部プラグインが不要になり、Gulp 単体でできることが増えたので、パッケージ管理が容易になったと思う。

参考文献