getalog

console.log geta6

gulpをstreamとか関係なくただのタスクランナーとして使う

gulpはstream志向でデザインされていて、streamしか受け入れない・streamじゃないとon the railじゃない、というようなイメージが強いと思う。

ところがどっこい、gulpのタスクが受け入れるのはstreamだけじゃないし、必ずしもgulp-*とかvinylとかを使わなければならない理由も特に無い。それらを使わなくてもタスクは実行できる。

「stream使わなくてもいいじゃん」と割り切ると、gulpの使い途が広がる。

一応挙げておくと、例えば下記のコードは正しいタスク。

gulp.task('synctask', () => {
  console.log('sync task executed.');
});

非同期であれば下記のように書ける。

gulp.task('asynctask', done => {
  setTimeout(() => {
    console.log('async task executed.');
    done();
  }, 1000);
});

orchestrator

ところで、gulpのdependenciesを見るとorchestratorというものがある。

orchestratorはgulpのようなタスクランナーツールのベースを提供するパッケージで、コールバックを渡せばコールバックが呼ばれるまで待つし、Promiseを返せばPromiesがresolveするまで待ってくれるAPIを提供している。

単純に言えば、gulpはこのorchestratorにstreamインターフェースを乗せたり、CLIツールを提供したり、interpretrun-sequenceといった便利なツールが出揃ったりして、色々と便利になったものに過ぎない。

ここで重要なのが、orchestratorがPromiseインターフェースを持っている点(タスク内でPromiseをreturnすると、そのPromiseがresolve/rejectされるまで待ってくれる)。

ということは、gulpもPromiseのインターフェースを持っている。Promiseを受け入れられるということは、async/awaitが使える。

async/awaitを使うと、streamを使わないgulpタスクがめっちゃスッキリ書ける。

del

例えばこんなタスクがある。

gulp.task('clean', done => {
  del(['build/*', 'tmp/*'], { dot: false }, () => {
    console.log('clean upped!');
    done();
  });
});

delはPromiseを返すので、下記のように書くこともできる。

gulp.task('clean', async () => {
  await del(['build/*', 'tmp/*'], { dot: false });
  console.log('clean upped!');
});

今までdelでファイルを削除した後にさらに何かしたい時は、自分で定義したコールバックの中でgulpからやってきたコールバック(done)を呼ぶ必要があった。だるい。

async/awaitを使うことでかなりシンプルになったと思う。

run-sequence

run-sequenceでも書き直してみる。

import run from 'run-sequence';

gulp.task('watch', done => {
  run(['clean', 'build', 'serve'], () => {
    gulp.watch('src/**/*.{js,jsx}', () => run('build', 'serve'));
    done();
  });
});

これをasync/awaitで書き直すとこうなる。

gulp.task('watch', async () => {
  await new Promise(resolve => run(['clean', 'build', 'serve'], resolve));
  gulp.watch('src/**/*.{js,jsx}', () => run('build', 'serve'));
});

run-sequenceはundefinedを返すが、コールバックを受け入れてくれるのでPromiseでwrapしてawaitすればよい。

あるいは、頻繁に使うなら最初からwrapしたfunctionを定義すればよい(これより下の例ではそうしている)。

copy

copyとかもgulp.srcを使わず、一般的なnodejsの資産(例えばncp)を使って実装する方法もある。

import ncp from 'ncp';

const copy = (source, destination) => new Promise((resolve, reject) => {
  ncp(source, destination, err => err ? reject(err) : resolve());
});

gulp.task('copy', async () => {
  await Promise.all([
    copy('src/index.html', 'build/index.html'),
    copy('package.json', 'build/package.json'),
  ]);
});

watch

gulp.watchを使わず、基盤パッケージであるgazeを使って書くこともできる。

import gaze from 'gaze';

const WATCH = process.argv.includes('--watch');

const watch = pattern => new Promise((resolve, reject) => {
  gaze(pattern, (err, watcher) => err ? reject(err) : resolve(watcher));
});

gulp.task('copy', async () => {
  await Promise.all([
    copy('src/index.html', 'build/index.html'),
    copy('package.json', 'build/package.json'),
  ]);
  if (WATCH) {
    const watcher = await watch('src/**/*.html');
    watcher.on('changed', async file => {
      await copy(file, `build/${path.basename(file)}`);
    });
  }
});

webpack

webapckの監視サーバーを立ち上げて差分ビルドするタスクをgulp-*のstreamインターフェースで実現しているパッケージは(たぶん)無いが、自前で書けば普通にできる。

import webpack from 'webpack';
import util from 'gulp-util';

gulp.task('bundle', async () => {
  let config = require('./webpack.config.babel');
  config = Array.isArray(config) ? config : [config];
  await new Promise((resolve, reject) => {
    let count = 0;
    const bundler = webpack(config);
    const bundle = (error, stats) => {
      if (error) {
        reject(new util.PluginError('bundle', error.message));
      } else {
        util.log(stats.toString(config[0].stats));
        if (++count === (WATCH ? config.length : 1)) {
          resolve();
        }
      }
    };
    WATCH ? bundler.watch(200, bundle) : bundler.run(bundle);
  });
});

まとめ

nodejsの資産を使えばgulp・gulp周辺ライブラリへの依存度を下げることができる(じゃあgulp使うなよとか言われそう)。

streamに拘泥してマイナーなgulpライブラリ使ったり、慣れない実装でバグ産んだりするくらいだったら、自分が慣れてる方法で実装したほうが早いし確実。