gulpで消耗したのでgulp(Stream)について調べてみる。

飽きたので途中で掲載。

はじめに

gulpの使い方と言う話が出てきた時に以下の2つの場合がある。

  • gulp(plugin)の使い方
  • gulp(plugin)の作り方

大抵は前者で、前者はとても簡単。

gulp(plugin)の使い方

以下のようなシェルスクリプトのコマンドをイメージしてみれば良い。

$ src | foo -v | bar -x | boo -y | dst

これをjsで行なっているのがgulp。以下のように書くと思えば良い。

gulp.src("./src/*.js")
    .pipe(foo({"v-flag": true}))
    .pipe(bar({"x": true})
    .pipe(boo({"y": true})
    .pipe(gulp.dst("./dist"));

ただしgulpのpluginは実際に実行されるaction(これは便宜的な呼び方)ではなく、actionのfactoryになっている。 具体的にはoptions(設定項目)を受け取ってactionを生成する。

var actionA = pluginA(options);
src.pipe(actionA).pipe(dst());

丁寧に書くとこのようにして使う。おしまい。

何かやりたければまず最初にrecipesを見ると良い。

gulp(plugin)の作り方

こちらが本題。 そもそもgulpはシェルスクリプトのpipeを利用したインターフェイスを提供することでタスクランナーを作ったらどうかというような発想で作られたらしい。 今まで、具体的な内部構造については言及していなかった。 裏側ではStreamが使われているとのこと。

Streamと言うのは流れということで何らかのパイプラインをイメージしてもらえば良いらしい。 パイプの中を何が流れているかは異なる可能性がある。

Streamについて

Streamには以下の4種類がある

  • ReadableStream: データの生成元
  • WritableStream: データの出力先
  • TransferStream: データの変換
  • DuplexStream: 双方向通信

gulpにおいては、ReadableStream・WritableStreamの部分はgulp.src・gulp.dstを使うというふうに考えて良い。 したがって、実際のところ、気にするのはTransferStreamをどうやって作るかと言う話になる。

そこで、ここでは既に以下のようなReadableStream,WritableStreamが既に定義されているということにして進める。

// 0から2までの数値を生成=numsし、結果を出力する=display
var s = require('./s');
s.nums(3).pipe(s.display("i: %s"));
/*
i: 0
i: 1
i: 2
*/

上にあげた入力と出力の間に挟めて、何らかの変換をするstreamのことを考えることにする。

TransferStreamの作り方

TransferStreamは、stream.Transformから作る事ができる。 例えば条件を取ってその結果にマッチしていない場合にはエラーになるTransferStreamを作ってみよう。 以下の様にして使える。

function negative(n){
  return n < 0;
}
// invalid value
// s.nums(3).pipe(guarded(negative)).pipe(s.display("%s"));

ドキュメントを読む限り以下の様にして定義する事ができる。

  • stream.Transformを継承して作る
  • _transform(chunk, encoding, callback) という関数を実装する
  • _flush(callback) という関数を実装する

_transform()はthis.push()を呼んだものを先に通し、エラーの場合にはエラーオブジェクトをemitしてあげれば良い。 (実際のgulp pluginではgulp-util.PluginErrorをemitするのが良いかもしれない)

受け取ったcallback(ここではdone)は必ず呼び出す必要がある。そうでないと流れが堰き止められてしまう。

var Transform = require("stream").Transform;
var util = require('util');

function Guarded(predicate){
  var opts = {objectMode: true};
  Transform.call(this, opts)
  this.predicate = predicate
}
util.inherits(Guarded, Transform);

Guarded.prototype._transform = function(chunk, encoding, done){
  if(this.predicate(chunk)){
    this.push(chunk)
  }else{
    this.emit("error", new Error("invalid value is flowed"));
  }
  done();
};

Guarded.prototype._flush = function(callback){
  callback();
};

module.exports = function guarded(p){
  return new Guarded(p);
};

最もgulpなどのタスク処理では、通常、このような関数は役に立たない。 条件を受け取るフィルターとして幾つかの種類がある。

  • (条件に合致しない場合壊れる)
  • 条件に合致したものだけを先に通す
  • 条件に合致したものにだけ反応を返し、それ以外は素通り

このようなオブジェクトを毎度作成するのは面倒ではある。 なのでTransferStreamを生成するために便利な関数が用意されている。 through2 が使える。

今回は、条件に合致したものだけを先に通す を実装してみることにする。

Streamの内部をchunkが流れるかobjectが流れるかを選ぶためのobjectModeという引数が存在しているが、 これはthrough2.objを使った場合にはdefaultでtrueが設定されるらしい。

var through = require("through2");

var separated = function(predicate){
  function transform(n, encoding, done){
    if(predicate(n)){
      this.push(n);
    }
    done();
  }
  return through.obj(transform);
}

s.nums(3)
  .pipe(separated(function(n){return n % 2 == 0;}))
  .pipe(s.display("%s"));

// 0
// 2

偶数の場合だけ通り抜けるようになった。実際の所このような関数もあまり使いみちは無いかもしれない。 変換中に、パイプの中を流れている途中で紛失してしまうというという状況は、何かを取りこぼしてしまったということになるので。流れてきた要素をすぐに下流に渡さず、幾つかの要素をマージしてから下流に流すなどには使うかもしれない。

条件に合致したものにだけ反応を返し、それ以外は素通り 直接的にこれというわけではないが、gulp-if というパッケージが存在している。 本来は、このgulp-ifが使えるのだが、今回は例のためにstream内を流れるオブジェクトを数値にしているので使えない。

// through2-mapを使っても良い。
var twice = function(){
  return through.obj(function(n, enc, done){
    this.push(n * n);
    done();
  });
}

var gif = require('./gulp-if-like');
s.nums(3)
  .pipe(gif(function(n){return n % 2 == 0;}, twice()))
  .pipe(s.display("%s"));
// 0
// 1
// 4

注意点として、gulp-ifの第2引数に渡すのはcallbackではなく、streamだということ。 そうすることで以下の様な処理が書けるようになる。

var gif = require('gulp-if')

// 全てのファイルを変換対象にする場合
gulp.src(src)
    .pipe(convert({debug: True}))
    .pipe(gulp.dst(dst));

// 一部のファイルを変換対象にする場合
gulp.src(src)
    .pipe(gif(isCSSFile, convertCSS()))
    .pipe(gif(isJSFile, convertJS()))
    .pipe(gulp.dst(dst));

gulp-if()の受け取るインターフェイスと、pipe()が受け取るインターフェイスが揃っていることで、 あるgulp pluginが存在した時、特定のファイルだけを変換対象にしたい場合などに何か処置を施す必要がない。 単にgulp-ifに渡すだけで済む。

また結果を集めて1つにまとめたい場合にもthrough.objは使える。

function aggregate(){
  var store = [];
  return through.obj(
    function transform(n, enc, done){
      store.push(n);
      done();
    }, function flush(done){
      this.push(store);
      done();
    })
}

s.nums(3)
  .pipe(aggregate())
  .pipe(s.display("%s"));

// [ 0, 1, 2 ]

stream.on(, ) について

TODO:

gulp pluginの実際の作り方

TODO:

gulp(plugin)の組み立て方

gulp pluginを直接つくるということは少ないかもしれない。 少なくとも既にpluginが存在しているものに対して新たなものを作るのは避けるべきという話らしい。

ただいくつかのgulp pluginを1つに組み合わせたpluginを作りたいという場合はあるかもしれない。 以下のようにも書けはするが、順番などが前後してわかりづらい。

function compose(plugin1, plugin2){
  return function(stream){
    return stream.pipe(plugin1).pipe(plugin2);
  }
}

compose(twice(),twice())(s.nums(3)).pipe(s.display("%s"));

このようなときにはlazypipeを使うと良い。

var lazypipe = require("lazypipe");

var compute = lazypipe()
    .pipe(twice)
    .pipe(twice)

s.nums(4).pipe(compute()).pipe(s.display("%s"));
// 0
// 1
// 16
// 81

pluginの書き方

まじめに作るなら ガイドライン に従うべき。

wrigin-a-pluginというドキュメントもあるのでこれを覗いてみると良い。

tips

optionによって処理を切り変えたい場合にはgulp-util.envを見れば良い。 例えばdebug時などで値を切り替えたい場合に便利かもしれない。

// $ gulp --type production
gulp.task('scripts', function() {
  gulp.src('src/**/*.js')
    .pipe(concat('script.js'))
    .pipe(gutil.env.type === 'production' ? uglify() : gutil.noop())
    .pipe(gulp.dest('dist/'));
});

参考