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ツールを提供したり、interpretやrun-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ライブラリ使ったり、慣れない実装でバグ産んだりするくらいだったら、自分が慣れてる方法で実装したほうが早いし確実。