Skip to content

Latest commit

 

History

History
299 lines (204 loc) · 16.2 KB

File metadata and controls

299 lines (204 loc) · 16.2 KB

gulp

この文章はgulp 3.9.0を元に書かれています。

gulpはNode.jsを使ったタスク自動化ツールです。 ビルドやテストなどといったタスクを実行するためのツールで、 それぞれのタスクをJavaScriptで書くことができるようになっています。

タスクは複数の処理の実行順序を定義したものとなっていて、タスクを定義するAPIとしてはgulp.taskが用意されています。 また、それぞれの処理はNode.jsのStreamとして実装することで、 処理をStreamでつなげる(pipe)することができ、複数の処理を一時ファイルなしでできるようになっています。

それぞれの処理はgulpのプラグインという形でモジュール化されているため、 利用者はモジュールを読み込み、pipe()で繋ぐだけでタスクの定義ができるツールとなっています。

どう書ける?

例えば、Sassで書いたファイルを次のように処理したいとします。

  1. sass/*.scssのファイルを読み込む
  2. 読み込んだsassファイルをsassでコンパイル
  3. CSSとなったファイルにautoprefixtureで接頭辞をつける
  4. CSSファイルをそれぞれminifyで圧縮する
  5. 圧縮した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プラグインを書きながら、どのような仕組みで処理同士が連携を取って動作しているのかを見ていきましょう。

先ほどのgulpのタスクの例では、既にモジュール化された処理をpipeで繋げただけであるため、 それぞれの処理がどのように実装されているかはよく分かりませんでした。

ここではgulp-prefixerという、それぞれのファイルに対して先頭に特定の文字列を追加するgulpプラグインを書いていきます。

同様の名前のプラグインが公式のドキュメントで「プラグインの書き方」の例として紹介されているので合わせて見ると良いでしょう。

多くのgulpプラグインはオプションを受け取り、NodeのStreamを返す関数として実装されます。

import gulp-prefixer.js

ここで実装したgulp-prefixerは、次のようにしてタスクに組み込むことができます。

import gulpfile.babel.js

このdefaultタスクは次のような処理が行われます。

  1. ./*.*にマッチするファイルを取得(全てのファイル)
  2. 取得したファイルの先頭に"prefix text"という文字列を追加する
  3. 変更したファイルをbuild/ディレクトリに出力する

Stream

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タスクの処理をそれぞれ当てはめると次のようになっています。

  1. ./*.*にマッチするファイルを取得 = Readable Stream
  2. 取得したファイルの先頭に"prefix text"という文字列を追加する = Transform Stream
  3. 変更したファイルを build/ ディレクトリに出力する = Writable Stream

あるファイルを Read して、 Transform したものを、別のところに Write としているというよくあるデータの流れと言えます。

gulp-prefixer.jsでは、gulpから流れてきたデータをStreamで受け取り、 そのデータを変更したもの次へ渡すTransform Streamとなっています。

「gulpから流れてきたデータ」を扱うためにreadableObjectModewritableObjectModeをそれぞれ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用に作成された抽象フォーマットです。

なぜこういった抽象フォーマットが必要なのかは次のことを考えてみると分かりやすいと思います。

gulp.srcで読み込んだファイルの中身のみが、Transform Streamに渡されてしまうと、 Transform 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が格納されています。 そのため両方のパターンに対応したコードする場合はどちらが来ても問題ないように書く必要があります。

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プラグインと呼ばれるものは、gulpPrefixerprefixBufferにあたる部分だけを実装しています。

つまり、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
  • 既存のライブラリをラップしたプラグインが作りやすい
  • 同様の機能を持つプラグインが登場しやすい