Skip to content

Instantly share code, notes, and snippets.

@podhmo
Created October 20, 2015 15:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save podhmo/e6d8b1dbba26514817f8 to your computer and use it in GitHub Desktop.
Save podhmo/e6d8b1dbba26514817f8 to your computer and use it in GitHub Desktop.

[gulp][node.js]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してあげれば良い。

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で設定されるらしい。

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内を流れるオブジェクトをにしているので使えない。

// through-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の第一引数に渡すのはcallbackではなく、streamだということ。 そうすることで以下の様な処理が書けるようになる。

// 全てのファイルを変換対象にする場合
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));

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

また結果を集めて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);
    })
}

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);
  }
}

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

```javascript
var lazypipe = require("lazypipe");

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

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

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


## pluginの書き方

まじめに作るなら [ガイドライン](https://github.com/gulpjs/gulp/blob/master/docs/writing-a-plugin/guidelines.md) に従うべき。

- [gulp/docs/writing-a-plugin at master · gulpjs/gulp](https://github.com/gulpjs/gulp/tree/master/docs/writing-a-plugin)

- TODO: vinyl

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

```javascript
// $ 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/'));
});

参考

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);
};
var through = require('through2');
var ternaryStream = require('ternary-stream');
function gulpIfLike(p, trueChild, falseChild, opts){
if(!trueChild){
throw new Error("trueChild is required!");
}
function classifier(n){
return !!p(n, opts);
}
return ternaryStream(classifier, trueChild, falseChild);
}
module.exports = gulpIfLike;
{
"name": "about-gulp",
"version": "1.0.0",
"description": "gulpの使い方と言う話が出てきた時に以下の2つの場合がある。",
"main": "guarded.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"gulp-if": "^2.0.0",
"gulp-util": "^3.0.7",
"lazypipe": "^1.0.1",
"through2": "^2.0.0"
}
}
// Readable: https://nodejs.org/api/stream.html#stream_class_stream_readable_1
var util = require('util');
var Readable = require('stream').Readable,
Writable = require('stream').Writable;
function Nums(n){
var opt = {};
opt.objectMode = true;
Readable.call(this, opt);
this.i = 0;
this.limit = n
};
util.inherits(Nums, Readable);
Nums.prototype._read = function(){
if(this.i >= this.limit){
return false; // stop
}
this.push(this.i);
this.i += 1;
};
function Display(fmt){
this.fmt = fmt;
var opt = {};
opt.objectMode = true;
Writable.call(this, opt);
}
util.inherits(Display,Writable);
Display.prototype._write = function(ob, encoding, callback){
console.log(this.fmt, ob);
callback(); // callback(err)
};
module.exports = {
nums: function nums(n){return new Nums(n);},
display: function display(fmt){return new Display(fmt);}
};
var s = require('./s');
//s.nums(3).pipe(s.display("%s"));
var guarded = require("./guarded");
function negative(n){
return n < 0;
}
// invalid value
// s.nums(3).pipe(guarded(negative)).pipe(s.display("%s"));
function positive(n){
return n >= 0;
}
s.nums(3)
.pipe(guarded(positive))
.pipe(s.display("%s"));
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"));
// through-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
function aggregate(){
var store = [];
return through.obj(
function transform(n, enc, done){
store.push(n);
done();
}, function flush(done){
this.push(store);
})
}
s.nums(3)
.pipe(aggregate())
.pipe(s.display("%s"));
function compose(plugin1, plugin2){
return function(stream){
return stream.pipe(plugin1).pipe(plugin2);
}
}
var lazypipe = require("lazypipe");
var compute = lazypipe()
.pipe(twice)
.pipe(twice)
// compose(twice(),twice())(s.nums(3)).pipe(s.display("%s"));
s.nums(4).pipe(compute()).pipe(s.display("%s"));
var gutil = require("gulp-util");
var myCompositeTask = lazypipe()
.pipe(gutil.env.type !== "production" ? gutil.noop : uglify);
// .pipe(f)
// .pipe(g)
// .pipe(h)
s.nums(3)
.pipe(myCompositeTask())
.pipe(s.display("%s"))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment