賢く使うBrowserify 第1回 Browserifyとは
Browserifyを使うと、Node.jsのモジュールシステムをブラウザでも利用できるようになります。第1回目はBrowserifyがどのようにモジュールの依存を解決するのか、その方法と仕組みを解説します。
はじめに
Browserify(ブラウザリファイ)はsubstack氏によって作られたNode.js製のツールです。
このツールはNode.jsのコアモジュールやnpmのモジュールをブラウザでも利用できるようにするというのが元々の目的でしたが、モジュール間の依存解決やファイルの結合を行うためのビルドツールとして使われることが多くなってきているようです。
本シリーズでは、Browserifyを使ったJavaScriptのモジュール管理について解説します。
JavaScriptでの依存関係の解決
まずは、なぜこのようなツールを使って依存関係の解決を行う必要があるかを見ていきましょう。
例えば次のように、foo.js、bar.js、main.jsという3つのファイルがあったとします。
// foo.js
(function() {
function foo() {
return 'foo!';
}
window.foo = foo;
})();
// bar.js
(function() {
function bar() {
return 'bar!';
}
window.bar = bar;
})();
// main.js
(function() {
var foo = window.foo;
var bar = window.bar;
var el = document.getElementById('box');
el.textContent = foo() + ' ' + bar();
})();
main.jsはfoo.jsとbar.jsの機能を使ってテキストを出力しています。つまり、main.jsはfoo.jsとbar.jsに依存しているということです。
このようにファイル間に依存関係がある場合、読み込む順番を間違えると思い通りに動きません。例えばscript要素で読み込む場合、このようになります。
<script src="./js/foo.js"></script>
<script src="./js/bar.js"></script>
<script src="./js/main.js"></script>
foo.jsとbar.jsに依存関係はないのでどちらを先に書いてもかまいませんが、main.jsの前に2つのファイルを読み込む必要があります。
最近ではGruntやgulpを使って複数のファイルを1つのファイルにまとめることが多いですが、その場合も依存関係を考えて順番を指定する必要があります。
gulp.task('scripts', function() {
return gulp.src([
'./js/foo.js',
'./js/bar.js',
'./js/main.js'
]).pipe(concat('bundle.js'))
.pipe(gulp.dest('./dist/'));
});
ファイルが3つぐらいであれば、上記のように依存関係を、手動で管理してもさほど大変ではありませんが、これが100や200といったファイル数になってくると、手動で管理するのはかなり大変です。
ほとんどのプログラミング言語ではこのような問題を解決するために、モジュールの仕組みが提供されています。しかし、JavaScriptの言語仕様自体では、モジュールの仕組みが提供されていませんでした。現在はECMAScript 6の仕様でmodulesの仕様が策定されていますが、サポートされている環境はまだほとんどありません。
そのため、独自にモジュールの仕様を決めてモジュールの管理を行おうという試みがいくつかあります。現状で使われているモジュール管理の仕様は大きく分けてAMD(Asynchronous Module Definition)とCommonJS Modulesの2つです。
AMDの代表的な実装は、以前CodeGridで紹介したRequireJSです。AMDについての解説はこちらの記事を参照してください。
CommonJSというのは主にサーバーサイドJavaScriptの仕様を統一するための仕様で、その中の一部にModulesがあります。Node.jsのモジュールシステムはこれを元にしており、require
やexports
を使ってモジュールの依存関係を解決します。Node.jsのモジュールシステムについては、次の記事を参照してください。
Browserifyを使うと、Node.jsと同じCommonJS Modulesで書かれたJavaScriptのファイルをブラウザで利用することができるようになります。
Browserifyの利用
それでは先ほどの例をCommonJS Modulesで記述し、Browserifyでビルドしてみましょう。先ほどの例は次のようになります。
// foo.js
function foo() {
return 'foo!';
}
module.exports = foo;
// bar.js
function bar() {
return 'bar!';
}
module.exports = bar;
// main.js
var foo = require('./foo');
var bar = require('./bar');
var el = document.getElementById('box');
el.textContent = foo() + ' ' + bar();
記事冒頭で紹介した手動での読み込みと、次のような違いがあるのがわかります。
- 依存モジュールを呼び出すのに
require
を使っている - 外部に機能を公開するために
window
でなくmodule.exports
を使っている (function() {...})()
という即時関数で囲っていない
CommonJSの仕様に従い、module.exports
に代入したオブジェクトを別ファイル(main.js)からrequire
で呼び出しています。また、Browserifyを利用するとNode.jsと同じようにファイルごとにスコープができるので、(function() {...})()
で囲んでグローバルスコープを汚染しないようにする記述が必要ありません。
当然このままだと動かないので、これをBrowserifyを使ってブラウザで利用できるように変換してみましょう。
Browserifyのインストールと実行
まずはBrowserifyをnpmでインストールします。
$ npm install -g browserify
次に、以下のようなディレクトリ構造でJavaScriptファイルを設置します。
.
└── js
├── foo.js
├── bar.js
└── main.js
そうすると以下のコマンドで変換されたJavaScriptを出力することができます*。
$ browserify js/main.js -o bundle.js
*注:Browserifyの実行
今回はbrowserifyコマンドを直接利用していますが、Gruntやgulpなどで利用することもできます。この方法についてはシリーズ後半で解説する予定です。
これで変換されたbundle.jsを得ることができます。bundle.js*は次のようになります。
*注:bundle.jsという命名
bundle.jsというのは出力後のファイル名なので、app.jsやout.jsなど、なんでもいいのですが、複数のファイルを束ねるという意味でbundle.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){
// bar.js
function bar() {
return 'bar!';
}
module.exports = bar;
},{}],2:[function(require,module,exports){
// foo.js
function foo() {
return 'foo!';
}
module.exports = foo;
},{}],3:[function(require,module,exports){
// main.js
var foo = require('./foo');
var bar = require('./bar');
var el = document.getElementById('box');
el.textContent = foo() + ' ' + bar();
},{"./bar":1,"./foo":2}]},{},[3]);
機械的に生成されたものなので、中で何をしているかはわからないと思いますが、main.jsやfoo.js、bar.jsなどの内容が含まれていることがわかります。
一見、ごちゃっとしたコードですが、これはきちんとブラウザで実行できるJavaScriptです。次のようにHTMLを作成して確認します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>browserify example</title>
</head>
<body>
<div id="box"></div>
<script src="bundle.js"></script>
</body>
</html>
Browserifyによる依存関係の解決
ここで重要なのは、Browserifyでビルドする際に指定したファイルはmain.jsのみだったということです。にもかかわらず、foo.jsやbar.jsの内容もbundle.jsに含まれ、正しい順番で読み込まれています。
これはBrowserifyが指定されたファイル内にrequire
という関数を見つけると、require
に指定してあるファイルを探し、依存関係を解決するからです。main.jsは次のように、foo.jsとbar.jsをrequire
しています(.js
は省略可能なので省略しています)。
// main.js
var foo = require('./foo');
var bar = require('./bar');
var el = document.getElementById('box');
el.textContent = foo() + ' ' + bar();
さらに、foo.jsやbar.jsにもrequire
があれば、そのファイルを探して依存関係を解決します。したがって、起点のファイル(この場合main.js)さえ指定すれば、次のように複雑な依存関係になっていても、自動で、芋づる式に依存関係を解決してくれるのです。
また、グローバルオブジェクト(window
)経由でなく、依存するモジュールをファイル内に書くことで、どのファイルに依存しているのかが、パッと見てわかるようになります。
グローバルオブジェクト経由だと、次のようにどのオブジェクトを利用するかはわかりますが、そのオブジェクトがどのファイルで定義されているかは、このファイルを見ただけではわかりません。
// FooModelはどこに定義されてるかわからない
var FooModel = window.FooModel;
// 処理
一方、ファイル内に依存関係を記述する場合はそのファイルを見ただけで、どのモジュールに依存しているかがパッと見てわかります。
// FooModelは ./model/foo.js に定義されているのが一目瞭然
var FooModel = require('./model/foo');
// 処理
まとめ
今回は依存関係の解決について解説し、ブラウザで利用するJavaScriptの依存関係をNode.jsと同じように解決するためのBrowserifyについて解説しました。
次回はBrowserifyによる処理をする前に、さまざまな変換を行うtransformsや外部ライブラリの組み込みなど、より実践的なBrowserifyの使い方について解説します。