昨年12月11日にめでたくGulp v4.0が正式リリースされたので、webサイト制作用gulpfile.jsの設定を見直してみました。

公式ドキュメント

迷ったらこちらを見直します。英語です。

Gulp4新機能おさらい

では新機能を確認します。
1. タスクを直列処理するseries()と並列処理するparallel()メソッドが追加。run-sequenceプラグインはもう必要ない
2. watch()タスクはchokidarを使用したものに変更。gulp-watchプラグインはもう必要ない
3. lastRun()を使用して差分ビルドが簡単に。gulp-changedプラグインはもう必要ない
4. symlink()でシンボリックリンクを作成しpackage.jsonとnode_modulesを使いまわすことが可能に
5. ソースマップ組み込みサポートが追加。gulp-sourcemapsプラグインはもう必要ない
6. エクスポートされたfunctionのtask登録
7. カスタム・レジストリ(タスクの分割)がもっと簡単にできるようになった
8. 条件付きビルドと段階的ビルドがいろいろとアップデートされた

よし完全に理解した(してない)。ということでそれぞれの項目の詳細を見ていきます。

1. タスクを直列処理するseries()と並列処理するparallel()メソッドが追加。run-sequenceプラグインはもう必要ない。

Gulp3でのタスク処理はtask()メソッドしかなく、また引数に配列を使っていましたが、Gulp4ではタスクを直列処理するseries()と並列処理するparallel()メソッドが追加されました。
引数に配列を使ったGulp3のgulpfile.jsがAssertionError: Task function must be specifiedというエラーを吐いて動かなくなるのでそこだけご注意ください。というか、ここだけ書き換えればとりあえずGulp3で使っていたgulpfile.jsを使い回すことができます。頑張りましょう。
なおそれぞれの使い分けは「Lintツールを通してからビルド」とか「BrowserSyncさせてからファイルをウォッチ」といった処理の順番が大きな意味を持つ時はseries()を、処理の順番を問わない時はparallel()を使えば良いというわけですね。parallel()の方が処理速度も速いです。

書き換え

Gulp3

gulpfile.js

gulp.task('a', ['b', 'c'])
Gulp4

gulpfile.js

gulp.task('a', gulp.series(gulp.parallel('b', 'c')))

コード量は増えましたが、コードの見通しはスッキリしたような。

2. watch()タスクはchokidarを使用したものに変更。gulp-watchプラグインはもう必要ない。

書き換え

Gulp3

gulpfile.js

gulp.watch('*.js', ['task']);
Gulp4

gulpfile.js

gulp.watch('*.js', gulp.task('task'));
// or
gulp.watch('*.js', gulp.series('task'));
// or
gulp.watch('*.js', gulp.parallel('task'));

なるほど…これはさっそくparallel()とか使ってみたくなります。

3. lastRun()を使用して差分ビルドが簡単に。gulp-changedプラグインはもう必要ない。

このあたりからはマイナーチェンジですね。今までプラグインで実現していた機能がいくつかGulpの公式機能として使えるようになっています。すごく便利というものもあれば、そもそも使ったことなかったわ…というものもありました。

書き換え

Gulp3

gulpfile.js

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
const changed = require('gulp-changed');
gulp.task('images', function() {
  gulp
    .src(paths.images.src)
    .pipe(changed(paths.images.dest))
    .pipe(imagemin(imageminOptions))
    .pipe(gulp.dest(paths.images.dest));
});
Gulp4

gulpfile.js

const gulp = require('gulp');
const imagemin = require('gulp-imagemin');
function images() {
  return gulp
    .src(paths.images.src, {
      since: gulp.lastRun(images)
    })
    .pipe(imagemin(imageminOption))
    .pipe(gulp.dest(paths.images.dest));
}

こちらもわずかにスッキリしました。体感ですが少し処理時間も高速化されたようです。とくにweb製作時の画像最適化など、数が多くて重いファイル処理時に力を発揮してくれそう。

4. symlink()でシンボリックリンクを作成しpackage.jsonとnode_modulesを使いまわすことが可能に。

これは使ったことがない機能でした。個人的にはこれからも使わないかも…。
ということでBefore After比較ができません。下の良記事の紹介にてお茶を濁すことをご容赦ください。

Gulp(ガルプ)で爆速コーディング!複数の案件でGulpを使い回してみよう

5. ソースマップ組み込みサポートが追加。gulp-sourcemapsプラグインはもう必要ない。

書き換え

Gulp3

gulpfile.js

const gulp = require('gulp');
const sourcemaps = require('gulp-sourcemaps');
const plumber = require('gulp-plumber');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');
gulp.task('scripts', function() {
  gulp
    .src(paths.scripts.src)
    .pipe(sourcemaps.init())
    .pipe(plumber())
    .pipe(concat('lib.js'))
    .pipe(uglify())
    .pipe(sourcemaps.write('./maps'))
    .pipe(gulp.dest(paths.scripts.dest));
});
Gulp4

gulpfile.js

const gulp = require('gulp');
const plumber = require('gulp-plumber');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');
function scripts() {
  return gulp
    .src(paths.scripts.src, { sourcemaps: true })
    .pipe(plumber())
    .pipe(concat('lib.js'))
    .pipe(uglify())
    .pipe(gulp.dest(paths.scripts.dest, { sourcemaps: './maps' }));
}

こちらはスッキリ感が強い。

6. エクスポートされたfunctionのtask登録

上でもすでにこの変更を反映した書き方になっていますが、タスクの書き方がシンプルになりました。

書き換え

Gulp3

gulpfile.js

gulp.task('someTask', function() {
    gulp
  //処理
});
Gulp4

gulpfile.js

function someTask() {
  return gulp
  //処理
}

7. カスタム・レジストリ(タスクの分割)がもっと簡単にできるようになった

要は1つのgulpfile.jsを肥大化させたくないときに、gulpタスクごとに独立したファイルで保存できるということですね。個人的にはこの機能も使った経験がなく使う予定もないかな…。
良記事と公式ドキュメント紹介にてお茶を濁すことをご容赦ください。

8. 条件付きビルドと段階的ビルドがいろいろとアップデートされた

非常に漠然としてますが、開発作業中はソースマップを表示、ビルドした時だけソースマップ除去」といった条件処理がプラグインなしでできるようになり、ドキュメントも少し充実した印象です。

Using Plugins – Gulp4 APIドキュメント

gulpfile.js

//Souremapsを削除
const del = require('delete');
function cleanMapFiles() {
  return del([paths.styles.map, paths.scripts.map]);
}
gulp.task('clean', cleanMapFiles);

Gulp4対応のgulpfile.js、こうなった

というわけでGulp4対応のgulpfile.jsはこのようになりました。

gulpfile.js

const assets = require('postcss-assets');
const autoprefixer = require('autoprefixer');
const babel = require('gulp-babel');
const browserSync = require('browser-sync').create();
const clean = require('postcss-clean');
const concat = require('gulp-concat');
const del = require('del');
const eslint = require('gulp-eslint');
const flexBugsFixes = require('postcss-flexbugs-fixes');
const gulp = require('gulp');
const htmlhint = require('gulp-htmlhint');
const header = require('gulp-header');
const imagemin = require('gulp-imagemin');
const mozjpeg = require('imagemin-mozjpeg');
const notify = require('gulp-notify');
const order = require('gulp-order');
const plumber = require('gulp-plumber');
const pngquant = require('imagemin-pngquant');
const postcss = require('gulp-postcss');
const prettify = require('gulp-prettify');
const rename = require('gulp-rename');
const replace = require('gulp-replace');
const rollup = require('gulp-better-rollup');
const sass = require('gulp-sass');
const scsslint = require('gulp-scss-lint');
const sorting = require('postcss-sorting');
const uglify = require('gulp-uglify');
const paths = {
  root: './src',
  html: {
    src: './src/html/**/*.html',
    dest: './dist/'
  },
  styles: {
    src: './src/sass/**/*.scss',
    dest: './dist/css',
    map: './dist/css/maps'
  },
  scripts: {
    src: './src/js/**/*.js',
    jsx: './src/js/**/*.jsx',
    dest: './dist/js',
    map: './dist/js/maps',
    core: 'src/js/core/**/*.js',
    app: 'src/js/app/**/*.js'
  },
  images: {
    src: './src/img/**/*.{jpg,jpeg,png,svg,gif}',
    dest: './dist/img/'
  }
};
// PostCSS
const autoprefixerOption = {
  grid: true
};
const sortingOptions = require('./postcss-sorting.json');
const postcssOption = [
  assets({
    baseUrl: '/',
    basePath: 'src/',
    loadPaths: ['img/'],
    cachebuster: true
  }),
  flexBugsFixes,
  autoprefixer(autoprefixerOption),
  sorting(sortingOptions)
];

// HTML整形
function html() {
  return gulp
    .src(paths.html.src, { since: gulp.lastRun(html)})
    .pipe(
      prettify({
        indent_char: ' ',
        indent_size: 2,
        unformatted: ['a', 'span', 'br']
      })
    )
    .pipe(gulp.dest(paths.html.dest));
}

// Sassコンパイル(非圧縮)
function styles() {
  return gulp
    .src(paths.styles.src, { sourcemaps: true })
    .pipe(
      plumber({
        errorHandler: notify.onError('<%= error.message% >')
      })
    )
    .pipe(
      sass({
        outputStyle: 'expanded'
      })
    )
    .pipe(replace(/@charset "UTF-8";/g, ''))
    .pipe(header('@charset "UTF-8";\n\n'))
    .pipe(postcss(postcssOption))
    .pipe(gulp.dest(paths.styles.dest, { sourcemaps: './maps' }));
}
// Sassコンパイル(圧縮)
function sassCompress() {
  return gulp
    .src(paths.styles.src)
    .pipe(
      plumber({
        errorHandler: notify.onError('<%= error.message %>')
      })
    )
    .pipe(
      sass({
        outputStyle: 'compressed'
      })
    )
    .pipe(replace(/@charset "UTF-8";/g, ''))
    .pipe(header('@charset "UTF-8";\n\n'))
    .pipe(postcss(postcssOption, [clean()]))
    .pipe(gulp.dest(paths.styles.dest));
}
// JSコンパイル
function scripts() {
  return gulp
    .src(paths.scripts.src, { sourcemaps: true })
    .pipe(order([paths.scripts.core, paths.scripts.app]))
    .pipe(
      babel({
        presets: ['@babel/env']
      })
    )
    .pipe(rollup('cjs'))
    .pipe(plumber())
    .pipe(concat('lib.js'))
    .pipe(uglify())
    .pipe(
      rename({
        suffix: '.min'
      })
    )
    .pipe(gulp.dest(paths.scripts.dest, { sourcemaps: './maps' }));
}
// 画像最適化設定
const imageminOption = [
  pngquant({
    quality: '70-85'
  }),
  mozjpeg({
    quality: 85
  }),
  imagemin.gifsicle(),
  imagemin.jpegtran(),
  imagemin.optipng(),
  imagemin.svgo({
    removeViewBox: false
  })
];
// 画像最適化
function images() {
  return gulp
    .src(paths.images.src, {
      since: gulp.lastRun(images)
    })
    .pipe(imagemin(imageminOption))
    .pipe(gulp.dest(paths.images.dest));
}
// マップファイル除去
function cleanMapFiles() {
  return del([paths.styles.map, paths.scripts.map]);
}

// HTML Lint
function htmlLint() {
  return gulp
    .src(paths.html.src)
    .pipe(htmlhint())
    .pipe(htmlhint.reporter());
}
// SASS Lint
function sassLint() {
  return gulp.src(paths.styles.src).pipe(
    scsslint({
      config: 'scss-lint.yml'
    })
  );
}
// ESLint
function esLint() {
  return gulp
    .src([paths.scripts.src, paths.scripts.jsx, '!./src/js/core/**/*.js'])
    .pipe(
      eslint({
        useEslintrc: true,
        fix: true
      })
    )
    .pipe(eslint.format())
    .pipe(eslint.failAfterError());
}

// ブラウザ自動更新&ウォッチ
const browserSyncOption = {
  port: 8080,
  server: {
    baseDir: './dist/',
    index: 'index.html'
  },
  reloadOnRestart: true
};
function browsersync(done) {
  browserSync.init(browserSyncOption);
  done();
}

function watchFiles(done) {
  const browserReload = () => {
    browserSync.reload();
    done();
  };
  gulp.watch(paths.styles.src).on('change', gulp.series(styles, browserReload));
  gulp.watch(paths.scripts.src).on('change', gulp.series(scripts, esLint, browserReload));
  gulp.watch(paths.html.src).on('change', gulp.series(html, browserReload));
}

gulp.task('default', gulp.series(gulp.parallel(scripts, styles, html), gulp.series(browsersync, watchFiles)));

gulp.task('clean', cleanMapFiles);
gulp.task('imagemin', images);
gulp.task('sass-compress', sassCompress);
gulp.task('build', gulp.series(gulp.parallel(scripts, 'imagemin', 'sass-compress', html), 'clean'));
gulp.task('eslint', esLint);
gulp.task('html-lint', htmlLint);
gulp.task('sass-lint', sassLint);
gulp.task('test', gulp.series(sassLint, esLint, htmlLint));

Sassタスクを2つ書いている箇所をスマートに条件わけできそうだなとか、HTMLのLintツールをhtmlLintではなくeslint-plugin-htmlにまとめる方がインデントとかの一貫性保つのに良さそうかなといったあたりが今後の課題。が、とりあえずいい感じのweb制作用オレオレ設定ファイルになったような気がします。

所感

総じて使いやすくなった気がします。開発チームの皆さんに感謝ですね。
YAGNIの原則には敬意を表しつつ、event-stream事件みたいなことが稀にあり動いていたものが動かなくなることもあるからなー結局なんかタイミング見つけて見直すのがいいんだよな、と感じています。

参考リンク

見直しに当たって公式ドキュメントの他に下記を参考にしました。ありがとうございます!

公式

迷ったら公式。

元記事はこちら

Gulp4がリリースされたのでgulpfile.jsをアップデートした