入門
賢く使うBrowserify
第1回 Browserifyとは

Browserifyを使うと、Node.jsのモジュールシステムをブラウザでも利用できるようになります。第1回目はBrowserifyがどのようにモジュールの依存を解決するのか、その方法と仕組みを解説します。

2015年03月19日発行

目次

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に依存しているということです。

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のモジュールシステムはこれを元にしており、requireexportsを使ってモジュールの依存関係を解決します。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>
<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)さえ指定すれば、次のように複雑な依存関係になっていても、自動で、芋づる式に依存関係を解決してくれるのです。

main.jsのrequireを起点にすべての依存関係を解決してくれる

また、グローバルオブジェクト(window)経由でなく、依存するモジュールをファイル内に書くことで、どのファイルに依存しているのかが、パッと見てわかるようになります。

グローバルオブジェクト経由だと、次のようにどのオブジェクトを利用するかはわかりますが、そのオブジェクトがどのファイルで定義されているかは、このファイルを見ただけではわかりません。

// FooModelはどこに定義されてるかわからない
var FooModel = window.FooModel;
// 処理

一方、ファイル内に依存関係を記述する場合はそのファイルを見ただけで、どのモジュールに依存しているかがパッと見てわかります。

// FooModelは ./model/foo.js に定義されているのが一目瞭然
var FooModel = require('./model/foo');
// 処理

まとめ

今回は依存関係の解決について解説し、ブラウザで利用するJavaScriptの依存関係をNode.jsと同じように解決するためのBrowserifyについて解説しました。

次回はBrowserifyによる処理をする前に、さまざまな変換を行うtransformsや外部ライブラリの組み込みなど、より実践的なBrowserifyの使い方について解説します。

外村 和仁
外村 和仁
フロントエンド・エンジニア

HTMLコーダー、JavaScriptプログラマ、PHP、Perlのプログラマといった職務を経験後、2010年に株式会社ピクセルグリッドに入社。大規模サイトの運用、開発の経験を活かしてバックエンドからフロントエンドまで幅広く担当。2015年、退社。好きな言語はJavaScriptとRuby。 著書に『ノンプログラマのためのJavaScriptはじめの一歩』(単著:技術評論社、2012年11月7日)があり、共著も多数。このほか、WEB+DB PRESS、Software Designなどにも寄稿。