アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Browserify を使ってみる

Web ブラウザ向けの JavaScript でも node.js や node-webkit みたいに require で依存モジュールを読み込めるという Browserify を試してみる。

  • 2014/12/23: 追記

    • この記事で書いた gulpfile.js には問題があるので続く Browserify を使ってみる 2 - Source Map で修正
    • この記事のサンプルと同じものが対象なので GitHub リポジトリへのリンクはタグで分けている

Browserify と require

HTML から参照する JavaScript 間に依存がある場合、通常は HTML 側の script タグの記述順で調整する。例えば jQuery を利用するスクリプトがあるならそれより先に jQuery を読み込む必要がある。そのため依存が増えるたびに HTML 側を修正する必要があり面倒。また JavaScript だけ見ても依存が分かりにくい。

こうした問題の対策として Browserify の提供する require 機能が役立つ。これを利用すると JavaScript 側で依存を定義できる。例えば main.js が sub.js を必要とする場合、通常であれば HTML 側で以下のように記述するものだが、

<script src="js/sub.js"></script>
<script src="js/main.js"></script>

require の場合は main.js 側で以下の用に宣言しておき、

var sub = require( './sub.js' );

// sub を利用する処理...

HTML 側は Browserify による依存解決を経て生成されたスクリプト (名前は自由だが、ここでは app.js としておく) だけ読みこめばよい。

<script src="js/app.js"></script>

依存を JavaScript 間で定義するようになればエントリー ポイントになるスクリプトを読むだけで済ませられる。そのため依存の増減による HTML 修正は不要となり、JavaScript は require で依存が明示されるため実装を理解しやすくなる。

Browserify を動かしてみる

Browserify の require がどのように実現されるかを理解するため、実際に動かしてみよう。まず Browserify をコマンドラインから利用するためグローバルにインストールする。

$ npm install -g browserify

次に以下の JavaScript を用意。ファイルは同じディレクトリ内に置くこと。

var sub1 = require( './sub1.js' );
var sub2 = require( './sub2.js' );

sub1( 'message' );
sub2( 'message' );

このスクリプトでは同一階層の sub1.js、sub2.js を require で参照している。

module.exports = function( message ) {
    alert( message );
};

main.js と sub2.js から参照されるスクリプト。module.exportsrequire に対応している。これを参照すると引数の内容を alert する関数が得られる。

module.exports = function( message ) {
    var sub = require( './sub1.js' );
    sub( 'sub2:' + message );
};

main.js から参照されるスクリプト。sub1.js を参照している。ターミナルでこれらのファイルが置かれたディレクトリに移動して以下のコマンドを実行。

$ browserify main.js -o app.js

すると app.js というファイルが生成されるので、これを読み込む以下の HTML を用意してブラウザで開いてみるとアラートが表示されるはず。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Test Browserify</title>
</head>
<body>
  <script src="app.js"></script>
</body>
</html>

Browserify の生成ファイルと require

Browserify をコマンドライン実行して生成された app.js をみると以下のようになっている。

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var sub1 = require( './sub1.js' );
var sub2 = require( './sub2.js' );

sub1( 'message' );
sub2( 'message' );
},{"./sub1.js":2,"./sub2.js":3}],2:[function(require,module,exports){
module.exports = function( message ) {
    alert( message );
};
},{}],3:[function(require,module,exports){
module.exports = function( message ) {
    var sub = require( './sub1.js' );
    sub( 'sub2:' + message );
};
},{"./sub1.js":2}]},{},[1]);

冒頭は require 関数の定義。それ以降に main.js、sub1.js、sub2.js が結合されている。各スクリプトは個別の関数スコープに分離されインデックスが割り当てられている。require からの参照は指定された文字列とインデックスの対応によって行われるようだ。

Web ブラウザの特別な機能を利用することなく JavaScript の範疇で実現しているところが素晴らしい。

結合時に関数スコープで包まれることを前提にするとグローバル汚染を防ぐためにスクリプト全体を無名の即時関数で囲む必要もなくなる。またスクリプト外への機能公開は module.exports を利用できるのでグローバルに自前の名前空間を定義するような手間も省ける。

ただし HTML 側から参照するスクリプトが Browserify 生成となるため、集団開発している場合は運用に注意が必要。

Git や SVN を利用している場合、共有のため生成したスクリプトをコミットしたくなるかもしれないが、この方法だとスクリプトが二重管理になり好ましくない。コストを掛けてでも各人に Browserify を利用したビルド環境を整えてそれを利用してもらうほうがよいだろう。

Bower、gulp.js と組み合わる

Browserify は gulp から利用できる。また eugeneware/debowerify を利用することで Bower でインストールした JavaScript パッケージを扱えるようになる。

下準備

ビルドに必要なモジュールを npm でインストール。gulp-usemin なども利用したいのであわせて入れておく。ちなみに gulp から利用するだけであれば Browserify をグローバルにインストールする必要はなくプロジェクトのローカルだけでよい。

$ npm install --save-dev browserify
$ npm install --save-dev connect
$ npm install --save-dev debowerify
$ npm install --save-dev del
$ npm install --save-dev gulp
$ npm install --save-dev gulp-minify-css
$ npm install --save-dev gulp-uglify
$ npm install --save-dev gulp-usemin
$ npm install --save-dev serve-static
$ npm install --save-dev vinyl-source-stream

次に package.json へ以下の記述を追加。

{
  "browserify": {
    "transform": [
      "debowerify"
    ]
  }  
}

これは Bower でインストールしたモジュールを Browserify の require から利用するための debowerify 用の設定である。例えば Bower で jQuery をインストールしていた場合、それを利用する JavaScript で

var $ = require( 'jquery' );

$( '.l-content' )
    .append( $( '<div>' ) );

のように Bower のパッケージ名で require できる。実際の jQuery はプロジェクトのルートからだと bower_components/jquery/dist/jquery.js にインストールされるのだが debowerify によってパスが解決され Browserify による結合へ含まれる。ちなみに Browserify の生成したスクリプト上のキーは "./../../bower_components/jquery/dist/jquery.js" となるようだ。

gulpfile.js

gulp によるビルド設定は以下のように定義してみた。

var gulp = require( 'gulp' );

/**
 * リリース用イメージを削除します。
 *
 * @param {Function} cb コールバック関数。
 */
gulp.task( 'clean', function( cb ) {
    var del = require( 'del' );
    del( [ './dist' ], cb );
} );

/**
 * プロジェクトをビルドして開発用イメージを scr フォルダに生成します。
 */
gulp.task( 'build', function() {
    var browserify = require( 'browserify' );
    var source     = require( 'vinyl-source-stream' );

    browserify( './src/js/main.js' )
        .bundle()
        .pipe( source( 'app.js' ) )
        .pipe( gulp.dest( './src/js' ));
} );

/**
 * ブロジェクトのリリース用イメージを dist フォルダに生成します。
 */
gulp.task( 'release', [ 'clean', 'build' ], function() {
    var uglify    = require( 'gulp-uglify' );
    var minifyCSS = require( 'gulp-minify-css' );
    var usemin    = require( 'gulp-usemin' );

    gulp.src( './src/img/**'  ).pipe( gulp.dest( './dist/img'  ) );

    gulp.src( './src/*.html' )
        .pipe( usemin( {
            css: [ minifyCSS() ],
            js:[ uglify() ]
        } ) )
        .pipe( gulp.dest( './dist' ) );
} );

/**
 * 開発用リソースの変更を監視して、必要ならビルドを実行します。
 */
gulp.task( 'watch', [ 'build' ], function () {
    gulp.watch( [ './src/js/*.js', '!./src/js/app.js' ], [ 'build' ]);
} );

/**
 * 開発用フォルダをルートにして HTTP サーバーを起動します。
 */
gulp.task( 'server', [ 'watch' ], function () {
    var connect     = require( 'connect' );
    var serveStatic = require( 'serve-static' );

    var app = connect();
    app.use( serveStatic( __dirname + '/src' ) );
    app.listen( 8080 );
} );

/**
 * gulp の既定タスクです。
 */
gulp.task( 'default', [ 'build' ] );

build タスクでは Browserify によるスクリプト生成を実行。成果物は開発用となる src フォルダへ出力、その場所は元スクリプトと同一階層にしている。プロジェクトを Git リポジトリで管理している場合は .gitignore に成果物となるスクリプトを指定してリポジトリから除外すること。

開発中は「スクリプト修正 → build タスク実行」を繰り返してゆく。ユニットテストも入れたほうがようだろうけど今回は割愛。

プロジェクトをリリースするときは release タスクを実行。これは Browserify の他、gulp-usemin による諸処理を実行して dist フォルダに成果物をまとめて出力する。

サンプル

Browserify、Bower、gulp を利用したプロジェクトをサンプルとして GitHub に公開してみた。

clone して README.md の Installation に書かれた手順を実行するとビルドを試せる。