FuseBox - 新進気鋭の JavaScript モジュールバンドラー

SPA ( Single Page Application ) のような 大規模 web アプリケーションを作っていると JavaScript のバンドル ( 依存関係を解決して単一のファイルにまとめること ) がどうしても必要になりがちです。僕はこれまでずっとバンドルツールに Browserify を採用していたのですが、FuseBox なるものが良さげという話を耳にしたので試してみました。

本エントリは、2017年7月14日の Gotanda.js #8 in Nextbeat で登壇した LT をフォローしたものです。

FuseBox って何?

一言で言うと多機能かつ高速なモジュールバンドラーです。フレームワーク含め、複数の JavaScript ファイル間の依存関係を解決して最終的に単一の JavaScript ファイルを生成します。JavaScript コードをひとつにまとめることで HTML からの読み込みがシンプルになります。

GitHub での最初のコミットが2016年10月とまだまだ新しいツールであり、最初から ES Module に標準で対応しているのも特徴です1)もちろん CommonJS や AMD にも対応しています。

TypeScript にデフォルトで対応

一般的にこの手のモジュールバンドラーは JavaScript ファイルにのみ対応していて TypeScript は別途それ用のプラグインを用意するものなのですが、 FuseBox はデフォルトで .ts / .tsx に対応しています。現状 TypeScript の最新バージョンにも対応しており、これにより『プラグインの保守が進んでないせいで新しいバージョンにアップデート出来ない』といったリスクを最小限に抑えられます。

はじめての FuseBox - 実際に動かしてみよう

手っ取り早く FuseBox がどんなものなのか理解するために実際に動かしてみましょう。ここでは Angular の公式サイトにあるこちらのチュートリアルを題材にします。

プロダクションコード ( .ts )
  • ファイル数 : 14
  • 累計コード行数 : 388
依存ライブラリ
- @angular/common v4.2.6
- @angular/compiler v4.2.6
- @angular/core v4.2.6
- @angular/forms v4.2.6
- @angular/http v4.2.6
- @angular/platform-browser v4.2.6
- @angular/platform-browser-dynamic v4.2.6
- @angular/router v4.2.6
- angular-in-memory-web-api v0.3.2
- core-js v2.4.1
- rxjs v5.4.2
- zone.js v0.8.1

それではyarn add コマンドで FuseBox をインストールします。

$ yarn add -D fuse-box

FuzeBox の定義ファイルを作成

Webpack や Rollup.js 同様、 FuseBox もバンドルのための定義ファイル ( .js ) を 作成します。

const {FuseBox} = require('fuse-box');
const fuse = FuseBox.init({
  homeDir: 'src/scripts',
  output: 'public/assets/$name.js',  // (1)
  sourceMaps: true,
  cache: true
});
fuse.bundle('app')                  // (2)
    .instructions(`> main.ts`)      // (3)
    .watch();
fuse.run();

このファイルは以下を定義しています。

  • TypeScript ファイルを JavaScript にトランスパイル
  • ライブラリなどの依存関係を解決
  • Source Map 有効化
  • キャッシュを有効化
  • バンドル後のファイル名を指定
  • バンドル実行するためのエントリファイルを指定
  • watch 機能を有効化

Webpack や Rollup.js ではオブジェクトハッシュ形式で宣言的に定義しますが、FuseBox はこのようにメソッド呼び出しをすることで定義します。

( 1 ) はバンドルして生成するファイルの出力先を指定します。ファイル名は固定値ではなく $nameとテンプレート形式で記述します。こうすることで ( 2 ) の bundle() メソッドの引数に渡した文字列をファイル名とした JS ファイルが生成されます。( 3 ) はそのファイルを生成するためのエントリポイントを指定します。ここではmaint.tsをエントリポイントに指定しています。先頭にある > というシンボルは『ファイルをロードすると自動的にバンドルを実行する』を意味してます。シンボルには他にもたくさんの種類があり、それらを組み合わせることでより複雑なエントリポイントの条件を指定できます。

watch 機能は watch() メソッドを呼び出すことで有効化します。ここまでがバンドル内容の定義であり、最後に run() メソッドを呼び出すことで処理が実行されます。

実行は node コマンドで行う

FuseBox は他のバンドラーのような cli コマンドが無いため、Node.js のコマンドから実行します。

$ node fuse
--------------------------
Bundle 'app'
    main.js
    app.module.js
    app.component.js
    app-routing.module.js
    heroes.component.js
    hero.service.js
    hero-detail.component.js
    dashboard.component.js
    in-memory-data.service.js
    hero-search.component.js
    hero-search.service.js
└──  (11 files,  22.3 kB) default
└── core-js 239.6 kB (307 files)
└── rxjs 720.2 kB (345 files)
└── process 3 kB (1 files)
└── object-assign-polyfill 1 kB (1 files)
└── zone.js 93.9 kB (1 files)
└── @angular/platform-browser-dynamic 7.7 kB (1 files)
└── @angular/compiler 1007.2 kB (1 files)
└── @angular/core 480.1 kB (1 files)
└── @angular/common 130.7 kB (1 files)
└── @angular/platform-browser 140.7 kB (1 files)
└── @angular/forms 204.3 kB (1 files)
└── @angular/router 215.6 kB (1 files)
└── @angular/http 75.1 kB (1 files)
└── http 288 Bytes (1 files)
└── angular-in-memory-web-api 49.1 kB (1 files)
size: 3.3 MB in 2s 878ms
  --------------
  → WARNING Library "zone.js" contains "browser" field. Set .target("browser") to avoid problems with your browser build!
  --------------
Done in 4.00s.

zone.js に関してなにやら警告が出ましたが、正常にバンドルされました ( ブラウザで動作確認済み ) 。とてもシンプルですね。

環境変数を使って development 用と production 用とで処理を分ける

cli 形式であれば --debug--watch といったオプションを指定することで development 用と production 用とでバンドル時の設定を分けることが出来ますが、FuseBox にはそのような受け口が用意されていません。

その場合は yargs を使いましょう。yargs はコマンド実行時に指定したオプションをタスク側で受け取ることができる Node モジュールです。

yarn コマンドでモジュールをインストールします。

$ yarn add -D yargs

これで node コマンドでも他の cli みたいなオプションを扱うことが出来ます。毎回 node コマンドを呼び出すのは手間なので、package.json に npm-scripts を記述すると良いでしょう。

{
  ⋮
  "scripts": {
    "script:dev": " node fuse --variant dev",
    "script:prod": "node fuse --variant prod"
  }
}

variant というキーに devprod という値をそれぞれ指定してコマンド実行するように定義しました。続いてこれらのオプション値を読み込むために先ほどの fuse.js を編集します。

const {FuseBox} = require('fuse-box');
const argv      = require('yargs').argv;
const dev = argv.variant === 'dev';
const fuse = FuseBox.init({
    homeDir: 'src/scripts',
    output: 'public/assets/$name.js',
    sourceMaps: dev,
    cache: dev
});
const app = fuse.bundle('app').instructions(`> main.ts`);
dev && app.watch();
fuse.run();

オプションで指定したキーと値は require('yargs').argv 内にオブジェクトハッシュとして格納されており、argv.variant でコマンドから指定した値を取得できます。なお、オプションのキーだけ指定した場合は boolean の true が格納されます。上記では開発中 ( dev ) のみ SourceMap、キャッシュ、watchが有効化されるようになっています。

# 開発時
$ yarn run script:dev
# デプロイ用ビルド時
$ yarn run script:prod

開発用 http サーバ機能あり

FuseBox は最初から開発用 http サーバ機能を搭載しています。起動方法はとても簡単で、 FuseBox.init() の戻り値のインスタンスから dev()メソッドを呼び出すだけです。

const fuse = FuseBox.init({
  homeDir: 'src/scripts/',
  output: 'public/$name.js'
});
fuse.dev({
  port: 8080,       // port 番号を指定。デフォルトは 4444
  root: 'public/',  // ルートディレクトリを指定。デフォルトは output 先となるディレクトリ
  httpServer: true  // http サーバ機能を有効化するかどうかを指定。デフォルトは true
});
fuse.run();

デフォルトでは http サーバとして起動しますが、 Socket サーバとして起動したい場合は socketURI プロパティを指定すれば OK です。

fuse.dev({
  socketURI: 'ws://localhost:3333'
});

HMR ( Hot Module Reload ) にも対応

HMR とは web ブラウザをリロードすることなくアプリケーションの JS ファイルを再読込する機能のことです2)Webpack では Hot Module Replacement と称しています。。通常、JS ないし TS ファイルを更新してビルド処理を走らせるとその内容をブラウザに反映させるためにリロードさせるものですが、これが不要となります。

利用方法は、FuseBox の Bundle インスタンスから hmr()メソッドを呼び出すだけで OK です。

const app = fuse.bundle('app')
  .instructions('> main.ts')
  .hmr();
fuse.dev();
fuse.run();

これで HMR が有効化されます。この場合 http サーバは FuseBox のそれを使うことが推奨されます。Browser-sync といった別の http サーバと併用すると、web ブラウザと Node.js 間でのソケット通信が競合して正常に更新されなくなります。

他のモジュールバンドラーと比較してみよう

JavaScript のモジュールバンドラーは他にもいろいろありますが、同じ条件の下で違いはあるのかを検証してみました。

Browserify

  • フロントエンド向け JavaScript バンドラーの草分け的存在
  • CommonJS モジュール仕様
  • tsifyプラグインを導入することで TypeScript のトランスパイルからバンドルまでを一気通貫で処理できる
  • 同様に babelify プラグインを導入することで Babel でのトランスパイルからバンドルまでを実現できる
  • CommonJS 仕様のため、モジュール読み込みのパスが文字列のまま残るので、Minify してもそのまま残るというデメリットがある

Webpack

  • 多機能なモジュールバンドラー
  • version 2.x より ES Modules に対応
  • Three Shaking 対応
  • ts-loaderプラグインを導入することで TypeScript のトランスパイルからバンドルまでを一気通貫で処理できる
  • CSS や画像もJS だけでなく、CSS 、HTML、画像ファイルも扱える
    • CSS は import 指定したファイルを取り込む
    • プラグインを導入することで SCSS / Sass, Stylus など AltCSS のトランスパイルが可能
    • 画像は Base64 化したデータを各種コード内に取り込む
  • ローカルサーバ機能あり
  • HMR 機能あり

Rollup

  • ES Modules 対応 ( デフォルトはこれ )
    • CommonJS, AMD, UMD 等にも対応
    • CommonJS モジュールで書かれたコードは専用のプラグインで ES Modules へ置換する必要がある
  • rollup-plugin-typescript プラグインを導入することで TypeScript のトランスパイルからバンドルまでを一気通貫で処理できる
  • Do One Thing and Do It Well がコンセプト

Angular Tutorial を各種モジュールバンドラーでバンドル

FuseBox Rollup.js Webpack Browserify
ファイルサイズ 3,477 KB 3,064 KB 3,522 KB 3,455 KB
所要時間 3.28 s 11.86 s 9.06 s 7.58 s

FuseBox のビルド所要時間が他のモジュールバンドラーに対してダブルスコア以上の大差を付けています。速さに定評のあるモジュールバンドラーとは聞いていましたが、まさかこれほどの差を見せつけられるとは思いもよりませんでした。

一方、ファイルサイズに関しては Rollup.js が頭一つ抜けているのがお分かりいただけるかと思います。これは Rollup.js がバンドル時に Tree Shaking を適用しているためです。これによりどこからも呼び出されない変数やメソッド、不要な条件式といったコードがごっそり削除されてファイルサイズが軽量化されます。Tree Shaking は Webpack にも搭載されていますが、Rollup.js ほど大胆には削除しないようです。

実は FuseBox にも TreeShaking 機能はあるのですが、適用してみると本来必要なコードまでが削除されてしまい、まともに動作しない JavaScript コードが生成されてしまいました。静的解析が甘いのでしょうか?

所感

以上、簡単ではありますが FuseBox についてご紹介してきました。やはり特筆すべきはその処理速度。他のモジュールバンドラーにダブルスコア以上の大差 を付けているのはそれだけで大きな魅力です。速いは正義。はじめから TypeScript に対応しているのも導入コストが低くて嬉しいポイントです。

設定ファイルの書き方は他のモジュールバンドラと異なっていたり、コマンドライン ( cli ) から実行出来ないというのは好みが別れるかもしれません。個人的には『Gulp の書き方に少し似てるかなぁ』という印象でした。

FuseBox は Webpack 同様多機能を売りにしていますが、TypeScript ファイルのバンドルのみの用途で使うのが吉 と思いました。お手軽にひとつのツールで完結したいというニーズがあるのは理解出来ます。しかし何でもかんでもそれひとつに頼ってしまうと、そのツールが壊れた途端にビルド環境が崩壊してしまうというリスクが付きまといます。また、TypeScript は最新バージョンに対応してるけど、Sass や Pug 用のプラグインが古いバージョンにしか対応していないせいでトランスパイラをアップデート出来ない といったリスクもつきまといます3)Gulp が正に今そうなっていますよね?。ローカルサーバは Browser-sync や http-server など専用の Node パッケージを使い、AltCSS のコンパイルも node-sass や stylus などを単独で使うほうがずっと安全でシンプルな構成を維持できます。

ちょっとした書き捨てのコードであれば FuseBox ひとつで全てカバーしてしまうのもありですが、保守・運用も見据えた開発環境を作るのであれば JavaScript ( TypeScript ) のバンドルに限定して使うのが良いでしょう。

脚注

脚注
1 もちろん CommonJS や AMD にも対応しています。
2 Webpack では Hot Module Replacement と称しています。
3 Gulp が正に今そうなっていますよね?