gulp
この文章はgulp 3.9.0を元に書かれています。
gulpはNode.jsを使ったタスク自動化ツールです。ビルドやテストなどといったタスクを実行するためのツールで、それぞれのタスクをJavaScriptで書くことができるようになっています。
ここでいうタスクとは複数の処理からなる処理の固まりのことです。このタスクを定義するAPIとしてgulp.taskが用意されています。また、それぞれの処理はNode.jsのStreamを使いつなげることで、複数の処理を一時ファイルなしでできるようになっています。
それぞれの処理は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-prefixerは与えられたそれぞれのファイルに対して先頭に特定の文字列の追加するプラグインです。
同様の名前のプラグインが公式のドキュメントで「プラグインの書き方」の例として紹介されているので合わせて見るとよいでしょう。
多くのgulpプラグインはオプションを受け取り、NodeのStreamを返す関数として実装されます。
import {Transform} from "stream";
export function prefixBuffer(buffer, prefix) {
return Buffer.concat([Buffer(prefix), buffer]);
}
export function prefixStream(prefix) {
return new Transform({
transform: function (chunk, encoding, next) {
const buffer = prefixBuffer(chunk, prefix);
this.push(buffer);
next();
}
});
}
const 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;
ここで実装したgulp-prefixerは、次のようにしてタスクに組み込むことができます。
import gulp from "gulp";
import gulpPrefixer from "./gulp-prefixer";
gulp.task("default", function () {
return gulp.src("./*.*")
.pipe(gulpPrefixer("prefix text"))
.pipe(gulp.dest("build"))
.on("error", (error) => {
console.error(error);
});
});
このdefaultタスクは次のような処理が行われます。
./*.*にマッチするファイルを取得(すべてのファイル)- 取得したファイルの先頭に"prefix text"という文字列を追加する
- 変更したファイルを
build/ディレクトリに出力する
Stream
gulp-prefixer.jsを見てみると、gulpPrefixerというTransform Streamのインスタンスを返していることが分かります。
const 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については以下を合わせて参照するといいでしょう。
vinyl
gulpではvinylオブジェクトがStreamで流れてきます。このvinylは Virtual file format という呼ばれているもので、ファイル情報と中身をラップしたgulp用に作成された抽象フォーマットです。
なぜこういった抽象フォーマットが必要なのかは次のことを考えてみると分かりやすいです。
- Streamで流れてきたデータの拡張子を知りたい
- Streamで流れてきたデータの読み取り属性をチェックしたい
- Streamで流れてきたデータと同じ場所にファイルを書き出したい
ファイルの中身だけがStreamで流れた場合は、ファイルのパスや読み取り属性などの詳細な情報を知ることができません。そのため、gulp.srcで読み込んだファイルはvinylでラップされ、ファイルの中身はcontentsとして参照できるようになっています。
vinylの中身を処理する
次は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が格納されています。そのため両方のパターンに対応したコードする場合はどちらが来ても問題ないように書く必要があります。
: 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が流れてくる
const 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ではプラグインがもつ機能は1つ(単機能)とすることを推奨しています。
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でも発生します。
たとえば、BrowserifyはNode.js Streamを扱えますが、変換の開始点としていない場合に問題が発生します。
また、gulpは単機能のプラグインを推奨していますが、これはAPIとしてそういう制限があるわけではないためあくまでルールとなっています。
このような問題に対してgulpはガイドラインやレシピといったドキュメントを充実させることで対処しています。
既存のライブラリをプラグイン化しやすい一方、プラグインとライブラリのオプションが異なったり、利用者はプラグイン化したライブラリの扱い方を学ぶ必要があります。
ライブラリとプラグインの作者が異なるケースも多いため、同様の機能をもつプラグインが複数できたり、質もバラバラとなりやすいです。
まとめると
- プラグインの組み合わせ問題は利用者が解決しないといけない
- 同様の機能をもつプラグインが生まれやすい
この仕組みを使っているもの
- sighjs/sigh
- gulpプラグインそのものをサポートしています。
まとめ
ここではgulpのプラグインアーキテクチャについて学びました。