この文章はgulp 3.9.0を元に書かれています。
gulpはNode.jsを使ったタスク自動化ツールです。 ビルドやテストなどといったタスクを実行するためのツールで、 それぞれのタスクをJavaScriptで書くことができるようになっています。
タスクは複数の処理の実行順序を定義したものとなっていて、タスクを定義するAPIとしてはgulp.task
が用意されています。
また、それぞれの処理はNode.jsのStreamとして実装することで、
処理をStreamでつなげる(pipe
)することができ、複数の処理を一時ファイルなしでできるようになっています。
それぞれの処理はgulpのプラグインという形でモジュール化されているため、
利用者はモジュールを読み込み、pipe()
で繋ぐだけでタスクの定義ができるツールとなっています。
例えば、Sassで書いたファイルを次のように処理したいとします。
sass/*.scss
のファイルを読み込む- 読み込んだsassファイルを
sass
でコンパイル - CSSとなったファイルに
autoprefixture
で接頭辞をつける - CSSファイルをそれぞれ
minify
で圧縮する - 圧縮したCSSファイルをそれぞれ
css
ディレクトリに出力する
この一連の処理は以下のようなタスクとして定義することができます。
import gulp from "gulp";
import sass from "gulp-sass";
import autoprefixer from "gulp-autoprefixer";
import minify from "gulp-minify-css";
gulp.task("sass", function() {
return gulp.src("sass/*.scss")
.pipe(sass())
.pipe(autoprefixer())
.pipe(minify())
.pipe(gulp.dest("css"));
});
ここでは、gulpプラグインの仕組みについて扱うので、gulpの使い方については詳しくは以下を参照してください。
- gulp/docs at master · gulpjs/gulp
- 現場で使えるgulp入門 - gulpとは何か | CodeGrid
- gulp入門 (全12回) - プログラミングならドットインストール
実際にgulpプラグインを書きながら、どのような仕組みで処理同士が連携を取って動作しているのかを見ていきましょう。
先ほどのgulpのタスクの例では、既にモジュール化された処理をpipe
で繋げただけであるため、
それぞれの処理がどのように実装されているかはよく分かりませんでした。
ここではgulp-prefixer
という、それぞれのファイルに対して先頭に特定の文字列を追加するgulpプラグインを書いていきます。
同様の名前のプラグインが公式のドキュメントで「プラグインの書き方」の例として紹介されているので合わせて見ると良いでしょう。
多くのgulpプラグインはオプションを受け取り、NodeのStreamを返す関数として実装されます。
ここで実装したgulp-prefixer
は、次のようにしてタスクに組み込むことができます。
このdefault
タスクは次のような処理が行われます。
./*.*
にマッチするファイルを取得(全てのファイル)- 取得したファイルの先頭に"prefix text"という文字列を追加する
- 変更したファイルを
build/
ディレクトリに出力する
gulp-prefixer.jsを見てみると、gulpPrefixer
というTransform Streamのインスタンスを返していることが分かります。
let gulpPrefixer = function (prefix) {
// enable `objectMode` of the stream for vinyl File objects.
return new Transform({
// Takes in vinyl File objects
writableObjectMode: true,
// Outputs vinyl File objects
readableObjectMode: true,
transform: function (file, encoding, next) {
if (file.isBuffer()) {
file.contents = prefixBuffer(file.contents, prefix);
}
if (file.isStream()) {
file.contents = file.contents.pipe(prefixStream(prefix));
}
this.push(file);
next();
}
});
};
export default gulpPrefixer;
Transform Streamというものが出てきましたが、Node.jsのStreamは次の4種類があります。
- Readable Stream
- Transform Stream
- Writable Stream
- Duplex Stream
今回のdefault
タスクの処理をそれぞれ当てはめると次のようになっています。
./*.*
にマッチするファイルを取得 = Readable Stream- 取得したファイルの先頭に"prefix text"という文字列を追加する = Transform Stream
- 変更したファイルを
build/
ディレクトリに出力する = Writable Stream
あるファイルを Read して、 Transform したものを、別のところに Write としているというよくあるデータの流れと言えます。
gulp-prefixer.jsでは、gulpから流れてきたデータをStreamで受け取り、 そのデータを変更したもの次へ渡すTransform Streamとなっています。
「gulpから流れてきたデータ」を扱うためにreadableObjectMode
とwritableObjectMode
をそれぞれtrue
にしています。
この ObjectMode というのは名前の通り、Streamでオブジェクトを流すための設定です。
通常のNode.js StreamはBufferというバイナリーデータを扱います。 このBufferはStringと相互変換が可能ですが、複数の値を持ったオブジェクトのようなものは扱えません。
そのため、Node.js StreamにはObject Modeがあり、これが有効の場合はBufferやString以外のJavaScriptオブジェクトをStreamで流せるようになっています。
Node.js Streamについては以下を合わせて参照するといいでしょう。
gulpではvinylオブジェクトがStreamで流れてきます。 このvinylは Virtual file format という呼ばれているもので、ファイル情報と中身をラップしたgulp用に作成された抽象フォーマットです。
なぜこういった抽象フォーマットが必要なのかは次のことを考えてみると分かりやすいと思います。
gulp.src
で読み込んだファイルの中身のみが、Transform Streamに渡されてしまうと、
Transform Streamからはそのファイルのパスや読み取り属性などの詳細な情報を知ることができません。
そのため、gulp.src
で読み込んだファイルはvinylでラップされ、ファイルの中身はcontents
として参照できるようになっています。
先ほどのTransform Streamの中身を見てみましょう。
// file は `vinyl` オブジェクト
if (file.isBuffer()) {
file.contents = prefixBuffer(file.contents, prefix);
}
if (file.isStream()) {
file.contents = file.contents.pipe(prefixStream(prefix));
}
vinyl
抽象フォーマットのcontents
プロパティには、読み込んだファイルのBufferまたはStreamが格納されています。
そのため両方のパターンに対応したコードする場合はどちらが来ても問題ないように書く必要があります。
NOTE: gulp pluginは必ずしも両方のパターンに対応しないといけないのではなく、Bufferだけに対応したものも多いです。しかし、その場合にStreamが来た時のErrorイベントを通知することがガイドラインで推奨されています。 - gulp/guidelines.md at master · gulpjs/gulp
contents
にどちらのタイプが格納されているかは、ひとつ前のStreamで決定されます。
gulp.src("./*.*")
.pipe(gulpPrefixer("prefix text"))
.pipe(gulp.dest("build"));
この場合は、gulp.src
により決定されます。
gulp.src
はデフォルトでは、contents
にBufferを格納するので、この場合はBufferで処理されることになります。
gulp.src
はオプションに{ buffer: false }
を渡すことでcontents
にStreamを流すことも可能です。
gulp.src("./*.*", { buffer: false })
.pipe(gulpPrefixer("prefix text"))
.pipe(gulp.dest("build"));
最後にBufferとStreamのそれぞれの変換処理を見てみます。
export function prefixBuffer(buffer, prefix) {
return Buffer.concat([Buffer(prefix), buffer]);
}
export function prefixStream(prefix) {
return new Transform({
transform: function (chunk, encoding, next) {
// ObjectMode:falseのTransform Stream
// StreamのchunkにはBufferが流れてくる
let buffer = prefixBuffer(chunk, prefix);
this.push(buffer);
next();
}
});
}
やってきたBufferの先頭にprefix
の文字列をBufferとして結合して返すだけの処理が行われています。
この変換処理自体は、gulpに依存したものはないため、通常のライブラリに渡して処理するということが可能です。
BufferはStringと相互変換が可能であるため、多くのgulpプラグインと呼ばれるものは、gulpPrefixer
とprefixBuffer
にあたる部分だけを実装しています。
つまり、prefixを付けるといった変換処理自体は、既存のライブラリで行うことができるようになっています。
gulpプラグインの仕組みはvinylオブジェクトのデータをプラグイン同士でやり取りすることで入力/変換/出力を行い、 そのインタフェースとして既存のNode.js Streamを使っていると言えます。
gulpのプラグインが行う処理は「入力に対して出力を返す」が主となっています。 この受け渡すデータとしてvinylオブジェクトを使い、受け渡すAPIのインタフェースとしてNode.js Streamを使っています。
gulpではプラグインは単機能であること推奨しています。
Your plugin should only do one thing, and do it well. -- gulp/guidelines.md
gulpは既存のNode.js Streamに乗ることで独自のAPIを使わずに解決しています。
元々、Transform Streamは1つの変換処理を行うことに向いていて、その変換処理をpipe
を繋げることで複数の処理を行う事できます。
また、gulpはタスク自動化ツールであるため、既存のライブラリをそのままタスクとして使いやすくすることが重要だと言えます。
Node.js Streamのデフォルトでは流れるデータがBuffer
であるため、そのままでは既存のライブラリでは扱いにくい問題を
データとしてvinylオブジェクトを流す事で緩和しています。
このようにして、gulpはタスクに必要な単機能のプラグインを既存のライブラリを使って作りやすくしています。 これにより再利用できるプラグインが多くできることでエコシステムを構築していると言えます。
gulpはそれ自体はデータの流れを管理するだけであり、タスクを実現するためにはプラグインが重要になります。 タスクには様々な処理が想定されるため、必要になるプラグインも種類が様々なものとなります。
gulpではvinylオブジェクトを中間フォーマットと決めたことで、 既存のライブラリをラップしただけのプラグインが作りやすくなっています。
またgulpは、Gruntとは異なり、タスクをJavaScriptのコードして表現します。 これにより、プラグインの組み合わせだけだと実現できない場合に、直接コードを書くことで対応するといった対処法を取ることができます。
そのため、プラグインの行う処理の範囲が予測できない場合に、gulpのように中間フォーマットとデータの流し方だけを決めるというやり方は向いています。
まとめると
- 既存のライブラリをプラグイン化しやすい
- 必要なプラグインがない場合も、設定としてコードを書くことで対応できる
プラグインを複数組み合わせ扱うものに共通することですが、プラグインの組み合わせの問題はgulpでも発生します。
例えば、BrowserifyはNode.js Streamを扱えますが、 先頭に置かないと他のプラグインと組わせて利用できない問題があります。
また、gulpは単機能のプラグインを推奨していますが、これはAPIとしてそういう制限があるわけではないためあくまでルールとなっています。
このような問題に対してgulpはガイドラインやレシピといったドキュメントを充実させることで対処しています。
既存のライブラリをプラグイン化しやすい一方、 プラグインとライブラリのオプションが異なったり、利用者はプラグイン化したライブラリの扱い方を学ぶ必要があります。
ライブラリとプラグインの作者が異なるケースも多いため、同様の機能を持つプラグインが複数できたり、質もバラバラとなりやすいです。
まとめると
- プラグインの組み合わせ問題は利用者が解決しないといけない
- 同様の機能を持つプラグインが生まれやすい
- sighjs/sigh
- gulpプラグインそのものをサポートしています。
ここではgulpのプラグインアーキテクチャについて学びました。
- gulpはタスク自動化ツール
- JavaScriptで設定を書くことができる
- gulpは中間フォーマットとデータの流れを決めている
- 中間フォーマットはvinylオブジェクト
- データの流れは既存のNode.js Stream
- 既存のライブラリをラップしたプラグインが作りやすい
- 同様の機能を持つプラグインが登場しやすい