watchify を試す

2015年2月21日 0 開発 , , , ,

Browserify による JavaScript コンパイルは便利だけど、ファイルが増えてくると処理時間の長さがネックになる。3 秒を超えたあたりから gulp.watch で監視するにはきつくなる感じ。

スクリプト間の依存を解決しつつ *fy 系の処理もこなすのだから仕方ないともいえる。とはいえ、ひとつファイルを変更しただけで構成ファイル全体がコンパイルされるのは防ぎたい。そのためには差分コンパイルが必要だ。というわけで watchify を試してみる。

2015/5/20 補足
本記事と関連する記事として「gulp-watchify を試す」を書いた。こちらは gulp プラグインで Browserify/watchify を処理している。

watchify

watchify は Browserify をファイル監視つきで実行するためのモジュールである。コマンドライン ツール、Node モジュールとして利用できる。今回は gulp タスクに組み込みたいので後者を選ぶ。 

gulp タスクで watchify を利用する場合、以下の記事が参考になる。

処理を要約すると、Browserify によるコンパイル部分を関数化し、ファイル更新を検知するたびに呼び出している。

ターミナルでタスクを実行すると gulp.watch を実行したときのようにプロセスが実行され、Ctrl + C などにより中断されるまでファイル監視を実行する。このとき、Browserify 管理下のファイルを変更すると、関連する部分だけが差分コンパイルされる。

コンパイルをともなうプログラミング言語にはリンカーやオブジェクト ファイルの概念があるものだけど、watchify の場合は gulp ストリーム内で処理が完結しているらしく、一時ファイルなどは生成されない。

そのため初回は常に全体コンパイルになる。

この動きを知らず gulp.watch に組み込んでしまうと、gulp.watch 自体の通知により毎回 watchify が起動し直されてしまい、全体コンパイルが繰り返される。おまけに watchify は監視を継続するので、どんどん数が増えてゆく。こうなると何重にもコンパイルが走ってしまうので、絶対に避けよう。

watchify を利用するタスク定義

参考記事を踏まえ、以下のようにタスクを実装してみた。

var gulp = require( 'gulp' );

/**
 * JavaScript の依存関係を解決し、単一ファイルにコンパイルします。
 * このタスクは開発用で、JavaScript は Minify されません。
 * Minify するとデバッガで Source Maps を展開したとき、変数名を復元できず不便なので開発時はそのまま結合します。
 *
 * @return {Object} gulp ストリーム。
 */
gulp.task( 'js', function() {
    return compile( false );
} );

/**
 * JavaScript の依存関係を解決し、単一ファイルにコンパイルします。
 * このタスクはリリース用で、JavaScript は Minify されます。
 *
 * @return {Object} gulp ストリーム。
 */
gulp.task( 'js-release', function() {
    return compile( true );
} );

/**
 * JavaScript の変更を監視して差分コンパイルします。
 *
 * @return {Object} gulp ストリーム。
 */
gulp.task( 'watchify', function() {
    return compile( false, true );
} );

/**
 * JavaScript の依存関係を解決し、単一ファイルにコンパイルします。
 *
 * @param {Boolean} isMinify 圧縮を有効にする場合は true。
 * @param {Boolean} isWatch  差分監視モードで実行する場合は true。
 *
 * @return {Object} gulp ストリーム。
 */
function compile( isUglify, isWatch ) {
    var $          = require( 'gulp-load-plugins' )();
    var config     = require( '../config.js' ).js;
    var errorUtil  = require( '../util/error' );
    var browserify = require( 'browserify' );
    var source     = require( 'vinyl-source-stream' );
    var buffer     = require( 'vinyl-buffer' );
    var watchify   = require( 'watchify' );

    var bundler = null;
    if( isWatch ) {
        var option = config.browserify;
        option.cache        = {};
        option.packageCache = {};
        option.fullPaths    = true;

        bundler = watchify( browserify( config.src, option ) );

    } else {
        bundler = browserify( config.src, config.browserify );
    }

    function bundle() {
        return bundler
            .bundle()
            .on( 'error', errorUtil )
            .pipe( source( config.bundle ) )
            .pipe( $.duration( 'compiled "' + config.bundle + '"' ) )
            .pipe( buffer() )
            .pipe( $.sourcemaps.init( { loadMaps: true } ) )
            .pipe( $.if( isUglify, $.uglify() ) )
            .pipe( $.sourcemaps.write( '.' ) )
            .pipe( gulp.dest( config.dest ) );
    }

    bundler.on( 'update', bundle );

    return bundle();
}

Browserify と watchify による JavaScript コンパイルを関数化して共有している。他のタスクと組み合わせるとき ( リリース用ビルドなど ) では watchify 監視しなくてもよいので、引数で動作を切り替えるようにした。

特記事項として gulp-duration によるコンパイル時間の通知がある。watchify は処理の終了をコンソール出力しないため、初回以降の処理時間を把握できない。また、出力がないせいで多重実行されても気づきにくい。そのためコンパイルの pipe に gulp-duration をはさみ、メッセージと処理時間を表示している。

gulp-duration の npm ページには watchify と組み合わせたサンプル コードが掲載されており、参考になる。

gulp.watch と組み合わせる

CSS などの監視に gulp.watch を利用しているとして、それらと watchify を組み合わせる方法を考える。

まず以下のようにそのまま gulp.watch へ指定すると、watchify が多重に起動されてゆく問題が起きる。この例だと依存タスクで 1 回、以降は *.js と *.jsx の変更を検知するたびに watchify が増える。

gulp.task( 'watch', [ 'watchify', 'css' ], function () {
    gulp.watch( [ './src/js/**/*.js', './src/js/**/*.jsx', '!./src/js/bundle.js' ], [ 'watchify' ] );
    gulp.watch( [ './src/stylus/*.styl', '!./src/css/*.css' ], [ 'css' ] );
} );

これを避けるには以下のようにする。そもそも watchify は一度だけ実行されればよいので、依存タスクにだけ watchify を指定しておく。

gulp.task( 'watch', [ 'watchify', 'css' ], function () {
    gulp.watch( [ './src/stylus/*.styl', '!./src/css/*.css' ], [ 'css' ] );
} );

このタスクを実行すると、単一の watchify と CSS 用の gulp.watch が並行で実行される。Ctrl + C などでプロセスを終了した場合は両方、止まってくれる。

サンプルプロジェクト

今回の調査で作成したサンプルを公開する。

プロジェクトのルートで npm i && bower i した後、gulp または gulp watch を実行すると watchify によるコンパイルを試せる。初回は時間がかかるものの、ひとつのスクリプトを変更して保存すると処理時間が大幅に短縮されていることを確認できる。

watchify によるファイル監視

watchify によるファイル監視

差分コンパイルできるとコンパイル時間の増加を恐れる必要がなくなるため、モジュールをより細かく分割したくなってくる。今回のサンプルでは React.js コンポーネントにおける render 部分だけ JSX にくくりだしてみた。この対応によりコンポーネント部分は通常の JavaScript シンタックスのみにできる。

MVVM でいうと JSX が View、これと Model を取り持つ JavaScript を ViewModel と位置づけた感じ。このように分割しておくと、あとで View を別のテンプレート エンジン ( jade とか ) で処理したくなったときの対応も楽そうだ。

Flux を利用するようになったら変えたくなりそうだけど、しばらくは WPF で親しんだ MVVM 的な構成で運用してみる予定。

以下、余談。

今回の調査中、気になることがあった。

サンプルでは Bower により React.js と normalize.css を管理しているのだが、これらは npm でも配布されている。そのため Bower をやめてパッケージ管理を npm に一本化してみようとした。

移行しても、モジュールのインストール先が bower_components から node_modules へ変わるだけだし、React.js の require も debowerify なしで読み込めており、正常動作している。しかし Firefox のインスペクタでソースを表示してみたら、React.js 関連のファイルが大量に並んでいた。

Firefox のインスペクタ表示

Firefox のインスペクタ表示

そのため自分のソースを探すのが難しい。ちなみに Chrome Developer Tools だとソース表示がツリーになっており、node_modules 由来のものはフォルダにまとめられるため問題ない。

結局、これを回避する手段が見つからなかったため npm 一本化は諦めた。将来、Firefox のインスペクタが Chrome のようになるか、設定などで不要なソース表示を除外 ( ソースのブラックボックス化も試したけどイマイチだった ) できるようになったとき、改めて一本化を再検討したい。

2015/4/30 追記
モジュールが大量に表示される問題は react/dist/react を参照することで解決できた。よってこれからはモジュール管理を npm に一本化する予定。

REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です