アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

gulp-watchify を試す

これまで Browserify/watchify 関連の gulp タスクは greypants/gulp-starter を参考に自前で定義していたのだが、処理の長さゆえ複数ファイルに分割せざるを得ず見通しがよくないと感じていた。

そこで Browserify/watchify 周りの機能を gulp プラグイン化した gulp-watchify を試してみる。

基本的な使い方

gulp-watchify のサンプル コードは GitHub リポジトリ内の examples ディレクトリに公開されている。

Transform は setup 関数内で実行、Uglify は pipe 上で処理しているようだ。しかし Browserify オプションについては触れられていない。Source Maps 出力などはどうすればよいのだろう?

高度?な使い方

Browserify を npm として直に利用する場合、関数の第二引数にオプションを指定することで Transform や Source Maps 出力などを設定できた。watchify を実行している場合はコンソールに開始時刻しか出力されずコンパイル時間がわからない。概算でよいからこの情報を出力できないものだろうか。

以上の要望をかなえるため、まずは gulp-watchify の仕組みを調べることにした。ソースを読んでみると index.js に以下のような処理がある。

function getBundle(file, opt) {
    // ...前略

    var bundle = browserify(opt)
    if (opt.watch !== false) {
        bundle = watchify(bundle, opt) // modifies bundle to emit update events
        cache[path] = bundle
        bundle.on('update', function() {
            bundle.updateStatus = 'updated'
            taskCallback(plugin)
        })
    }

    // ... 後略
}

gulp-watchify に指定したオプションはそのまま Browserify に渡されるようだ。それを拡張するように gulp-watchify 独自のプロパティを追加する設計となっている。つまり Browserify オプションのスーパーセットになるため Browserify の指定をそのままおこなえばよさそう。

次にコンパイル時間だが、これを出力するオプションは無さそう。なのでストリームの end イベントをハンドリングして gulp-util の log 関数を実行しておく。

これらを踏まえてタスクを実装すると、以下のようになる。

var gulp = require( 'gulp' );
var $    = require( 'gulp-load-plugins' )();

// 共通タスク設定
var common = {
  src:        './src',
  dest:       './dist',
  isWatchify: false,
  isUglify:   false
};

// js タスクのファイル監視モード実行フラグを有効化
gulp.task( 'js-enable-watchfy', function() { common.isWatchify = true; } );

// js タスクの圧縮・最適化モード実行フラグを有効化
gulp.task( 'js-enable-uglify', function() { common.isUglify = true; } );

// JavaScript 間の依存解決とコンパイルを実行し、その結果を単一のファイルとして出力
gulp.task( 'js', $.watchify( function( watchify ) {
  var buffer    = require( 'vinyl-buffer' );
  var formatter = require( 'pretty-hrtime' );
  var time      = process.hrtime();

  return gulp.src( [ common.src + '/js/App.js' ] )
    .pipe( $.plumber() )
    .pipe( watchify( {
      watch: common.isWatchify,
      basedir:   './',
      debug:     true,
      transform: [ 'babelify' ]
    } ) )
    .pipe( buffer() )
    .pipe( $.sourcemaps.init( { loadMaps: true } ) )
    .pipe( $.if( common.isUglify, $.uglify() ) )
    .pipe( $.rename( 'bundle.js' ) )
    .pipe( $.sourcemaps.write( './' ) )
    .pipe( gulp.dest( common.src ) )
    .on( 'end', function() {
      var taskTime = formatter( process.hrtime( time ) );
      $.util.log( 'Bundled', $.util.colors.green( 'bundle.js' ), 'in', $.util.colors.magenta( taskTime ) );
    } );
} ) );

// ファイル監視
gulp.task( 'watch', [ 'js-enable-watchfy', 'js' ], function () {
  // js 以外の gulp.watch
} );

このタスクで利用している npm は以下。

npm 用途
gulp gulp 本体。
gulp-if gulp の pipe 内で条件分岐する。
gulp-load-plugins gulp プラグインの読み込みを一括で実行し、キャメル ケースで呼び出す。
gulp-rename gulp の pipe 内で対象をリネームする。
gulp-plumber gulp タスクを watch モードで実行している際、エラーが発生しても中断せずに watch を継続させる。
gulp-sourcemaps JavaScript と CSS の Source Map 出力用。
gulp-uglify JavaScript の圧縮と最適化。
gulp-util gulp 関連のユーティリティ。今回は JavaScript コンパイル時間の出力に使用。
gulp-watchify Browserify/watchify を gulp ストリーム化して実行する。
browserify gulp-watchify 参照用。
watchify gulp-watchify 参照用。
babelify ES6 から 5 に変換するための Browserify プラグイン。
pretty-hrtime JavaScript コンパイル時間を文字列化するために使用。
vinyl-buffer ストリーム変換用。Source Map 出力などで必要。

素の Browserify/watchify から gulp-watchify に移行してもこれらの npm は必要なので注意。削除すると gulp-watchify を実行したときにエラーが起きる。

gulp-watchify のインストールで Browserify/watchify もセットでついてきた。gulp-watchify 内の node_modules にインストールされる想定だったがそうしない理由は不明。

gulp タスクの処理分岐ハックについて

サンプルの js-enable-watchfy と js-enable-uglify は gulp-watchify のサンプルを参考にしたハックである。gulp タスクにはオプションがないためグローバル変数にフラグを定義。それを変更するタスクを実装して依存指定することでタスクの処理分岐を実現している。

watch タスクではこれを利用して js-enable-watchfy で watchify を有効化したあと js タスクが実行されることを期待している。

// ファイル監視
gulp.task( 'watch', [ 'js-enable-watchfy', 'js' ], function () {
  // js 以外の gulp.watch
} );

しかし gulp タスクは標準で並列実行される。そのためこれで大丈夫なのか不安。例えば以下のように定義すれば mainsub の後に実行される。

gulp.task( 'main', [ 'sub' ], function() {
  // 処理
} )

しかし以下の定義だと sub1sub2 は並列実行されて、その後に main が実行される。

gulp.task( 'main', [ 'sub1', 'sub2' ], function() {
  // 処理
} )

main はよいとして sub1sub2 に依存関係がある場合、個別に依存タスクを定義して順番保証することになる。フラグを操作する程度であれば次のタスクを起動する前に終了しそうだから問題ないのかもしれない。とはいえ明示的に順番保証できていないのは気になる。

これを解決するには、関数化された gulp-watchify 処理を個別タスクで呼び分けるとよいだろう。gulp-starter のサンプルではそのようにしている。

function compile( isWatchfy, isUglify ) {
  // gulp-watchify による処理
}

gulp.task( 'js',          function() { return compile(); } );
gulp.task( 'js-watchify', function() { return compile( true ); } );
gulp.task( 'js-release',  function() { return compile( true, true ); } );

タスク外に関数を定義するのがイマイチ。なので本当に問題が起きた時だけ採用する予定。

Comments from WordPress

  • yuku_t yuku_t 2015-10-22T13:16:01Z

    フラグを操作する程度であれば、次のタスクを起動する前に終了しそうだから問題ないのかもしれない。とはいえ、明示的に順番保証できていないのは気になる。

    JavaScriptはシングルスレッドかつ 'js-enable-watchfy' は同期的に common.isWatching を書き換えているので(複数の依存タスクを指定した時にgulpが左から順に実行していくことを保証しているなら)必ず正しく動くのではないかと思いました。