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

昨年(2018年)12月11日にめでたく[Gulp v4.0が正式リリースされた](https://medium.com/gulpjs/version-4-now-default-92c6cd4beb45)ので、webサイト制作用gulpfile.jsの設定を見直してみました。

## 公式ドキュメント
迷ったらこちらを見直します。英語です。
- [入門ガイド](https://gulpjs.com/docs/en/getting-started/quick-start)
- [APIドキュメント](https://gulpjs.com/docs/en/api/concepts)

## Gulp4新機能おさらい
では新機能を確認します。
1. タスクを直列処理する`series()`と並列処理する`parallel()`メソッドが追加。[run-sequence](https://www.npmjs.com/package/run-sequence)プラグインはもう必要ない
2. `watch()`タスクは[chokidar](https://github.com/paulmillr/chokidar)を使用したものに変更。[gulp-watch](https://www.npmjs.com/package/gulp-watch)プラグインはもう必要ない
3. `lastRun()`を使用して差分ビルドが簡単に。[gulp-changed](https://www.npmjs.com/package/gulp-changed)プラグインはもう必要ない
4. `symlink()`でシンボリックリンクを作成しpackage.jsonとnode_modulesを使いまわすことが可能に
5. ソースマップ組み込みサポートが追加。[gulp-sourcemaps](https://www.npmjs.com/package/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
```js:gulpfile.js
gulp.task('a', ['b', 'c'])
``` 

##### Gulp4
```js:gulpfile.js
gulp.task('a', gulp.series(gulp.parallel('b', 'c')))
```

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

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

#### 書き換え
##### Gulp3
```js:gulpfile.js
gulp.watch('*.js', ['task']);
```

##### Gulp4
```js: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
```js: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
```js: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を使い回してみよう](https://yatteq.com/gulp-cording03) 

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

#### 書き換え
##### Gulp3
```js: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
```js: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
```js:gulpfile.js
gulp.task('someTask', function() {
    gulp
  //処理
});
```
##### Gulp4
```js:gulpfile.js
function someTask() {
  return gulp
  //処理
}
```

### 7. カスタム・レジストリ（タスクの分割）がもっと簡単にできるようになった
要は1つのgulpfile.jsを肥大化させたくないときに、gulpタスクごとに独立したファイルで保存できるということですね。個人的にはこの機能も使った経験がなく使う予定もないかな…。
良記事と公式ドキュメント紹介にてお茶を濁すことをご容赦ください。
- [gulpタスクの分割](https://qiita.com/dhun/items/c8633800097f1e7ecf70)
- [registry() - Gulp4 APIドキュメント](https://gulpjs.com/docs/en/api/registry#registry)

### 8. 条件付きビルドと段階的ビルドがいろいろとアップデートされた
「いろいろ」とは漠然としてますが、開発作業中はソースマップを表示、ビルドした時だけソースマップ除去」といった条件処理がプラグインなしでできるようになり、ドキュメントも少し充実した印象です。
[Using Plugins - Gulp4 APIドキュメント](https://gulpjs.com/docs/en/getting-started/using-plugins#conditional-plugins)

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

#### 追記: delについて
Gulp4では削除用パッケージが[rimraf](https://www.npmjs.com/package/rimraf)から[del](https://www.npmjs.com/package/del)に変更になっています。
delについてはこちらの記事（[gulp (v4) の削除タスク 〜 特定のディレクトリ/ファイルを除外](https://qiita.com/oreo3@github/items/1ea4d99cb0541323af9f)）が詳しかったのでご参照ください。

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

```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 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/',
  },
};
// Post CSS
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],{ base: './' }))
    .pipe(
      babel({
        presets: ['@babel/env'],
      }),
    )
    .pipe(plumber())
    .pipe(concat('lib.js'))
    .pipe(uglify())
    .pipe(
      rename({
        suffix: '.min',
      }),
    )
    .pipe(gulp.dest(paths.scripts.dest, { sourcemaps: './maps' }));
}
// 画像最適化
const imageminOption = [
  pngquant({
    quality: [0.7, 0.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]);
}

// ===Lint Tasks===
// 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の原則](https://ja.wikipedia.org/wiki/YAGNI)には敬意を表しつつ、[event-stream事件](https://qiita.com/yuta0801/items/012e09cb0fae5665cf27)みたいなことが稀にあり動いていたものが動かなくなることもあるからなー結局なんかタイミング見つけて見直すのがいいんだよな、と感じています。

## 参考リンク
見直しに当たって公式ドキュメントの他に下記を参考にしました。ありがとうございます！
- [How to Upgrade to Gulp 4 - Create default, parallel, series, and watch tasks](https://www.youtube.com/watch?v=2HpNiyimo8E): 英語ですが動画でわかりやすいです。
- [gulpではじめるWeb制作ワークフロー入門](https://nayucolony.github.io/gulp-support/): 書籍版も買いました。サンプルコードが嬉しいです。
- [Gulp 4.0 Upgrade Guide](https://zzz.buzz/2016/11/19/gulp-4-0-upgrade-guide/#upgrade-gulpfilejs)
- [gulp v4.0.0がプレリリース！ 移行方法と変更点まとめ](https://satoyan419.com/gulp-v4/)

### 公式
迷ったら公式。
- [入門ガイド](https://gulpjs.com/docs/en/getting-started/quick-start)
- [APIドキュメント](https://gulpjs.com/docs/en/api/concepts)