browserify から rollup.js に乗り換えてる

仕事でやってるブラウザjsのビルドには、長らくbrowserifyを使っていましたが、さいきんrollupに乗り換えました。

module bundler ツールたち

browserify、rollup、どちらも主眼に置いているのは、モジュール分割して書いたソースコードの依存解決です。必要なjsソースファイルをすべて辿り、展開して、まとめた1つのjsファイルをつくる、それが主な仕事です。 こうしたツールはmodule bundlerとか呼ばれています。

module bundler が必要な理由は、現状のブラウザではモジュールとして書かかれたjsの依存解決をすることができないからです。実現方法もまだ議論中みたいです。

また、大量のjsファイルをブラウザが読み込ませることは、パフォーマンス上のオーバーヘッドが無視できないという問題も相変わらずあり、現状では手元でせっせと依存解決して1つにまとめたjsをこしらえてから本番環境に置く、というワークフローが普通です。 module bundler は、このワークフローの面倒をまとめて見てくれる存在です。

開発中に書いたソースを本番用jsファイルをつくるには、モジュールの依存解決だけではありません。こんな変換もしたいです。

  • トランスパイル
    • es2015など新しい仕様のjsをブラウザで動く文法のjs変換 → babel、etc..
    • TypeScriptなどAltJS言語をブラウザで動くjsに変換 → TypeScript、etc..
  • 本番用ファイルを短縮、難読化してファイルサイズを減らす
  • ソースマップの出力

などなど。

この点、browserifyやrollup、あるいはwebpackもそうですが、プラグインを入れるだけで上記のような工程を簡単に追加できます。

module bundler は、人間が読み書きするjsを入力にとり、あれこれした後、最終的な成果物の実行用jsを吐くところまでに責任を持っているので、その間どのタイミングでどんな工程をはさむべきかを知っています。 そのためmodule bundler を使っていれば、パイプラインの順番に細かく気を配ったり、ビルドのためにスクリプトを書く必要もほぼなく、宣言するだけで、やりたいことが実現できます。最終的な出力とソースファイルを紐づける必要のある、ソースマップなどの機能も特別なことをしなくても利用できます。

gulpなどのjs製タスクランナーでパイプラインを組み立てる場合、自分でビルドのパイプラインをかなり詳細に設定することが可能ですが、それが必要になることは本当にごく稀だと思います。 よほど気合を入れてタスクを実装しない限り、 module bundler のパイプラインのほうが優れているし簡単です。コマンドラインからも利用できるので、 僕は コマンドを Makefile とか package.json とかに書いておいて使ってます。

CommonJS モジュール と ES モジュール

rollup は browserify や web pack より後発の module bundler で、 ESモジュールをそのまま解釈できることが大きな特徴です。

js の モジュール分割の仕様で、今後主流になるのは、es2015で標準化された ESモジュールだと思われます。

標準化されたのは node.js がだいぶ普及した後だったので、node.js では 別の仕様、CommonJS モジュールが採用され普及しています。 略して CJS モジュール です。

人気のある module bundler ツールの1つである browserify も、そもそもは CJSモジュールを ブラウザ js でもサポートする、というふれこみのツールでした。 ブラウザ js では、今後は 標準化された ESモジュールが使われていくことになると思われ、node.js 側でも、ESモジュールを利用できるようにするための議論がされているようです。 近い将来は、node モジュールは、CJS、ES 両方の仕様を提供するようになっていく模様ですが、もっと将来は、CJSモジュールはレガシーなものになっていくのではないでしょうか。

公開されている npm package も、最近のものは、ESモジュールが配布されていたりします。たとえば RxJS5 なんかは、ESモジュールファイルとして配布されるパッケージと、CommonJSモジュールとして配布されるパッケージが両方提供されていたりします。

rollup.js は、ES モジュール 形式で 配布されている npm パッケージ を、直接解釈することができます。それがbrowserify との大きな違いです。

browserify は、CommonJS モジュールをブラウザjs でもサポートすることがそもそもの目的だったので、ESモジュールで配布されているパッケージを直接読むこはできません。babel なんかは、ES モジュールを CJS に変換する 機能も持っているので、そういうツールを組み合わせ、CJSモジュールに変換してからあらためて解釈するという少し回り道なやりかたをとるしかありません。 ただ、babel はデフォルトでは npm パッケージを トランスパイル対象にはしないので、npm パッケージ側でCJS 、ES 両方のモジュール で提供してくれているパターンが多いです。

Tree-shaking

さて、rollup はただ仕様が新しいから気分的にすがすがしいというだけでなく、「吐いたjsファイルの容量が小さくなる」という実用面での大きなメリットがあります。

CommonJS モジュール、 ESモジュール、2つの仕様の違いは、シンタックスだけではなく、ESモジュール のほうがより static で explicit な仕様になってます。そもそも、js の良いところはめちゃめちゃにdynamicなところではあったのですが、近頃はコードベースが巨大になりがちなので、どちらかというと実用面での要請から 仕様が static めいてきてきているようです。ローカルスコープもクロージャもなにからなにまですべて function で表現できた時代は終焉を迎えてしまいました。

ES の import/ export は、式ではなく特別な文です。ソースファイルの トップレベルのスコープにしか置けない決まりになっていいます。このおかげで、 モジュールのソースを実行しなくても、パースすれば依存しているモジュールがわかるというわけです。

rollup は、この特徴を利用していて、依存モジュールを構文解析し、参照されていない オブジェクトや関数はソースコードをみつけて削除してくれます。これは Tree-shaking と呼ばれていて、公式サイト http://rollupjs.org の Webぺ上から試せるデモで、ファイルサイズが減る臨場感を感じることができます。

また、CommonJS では、 var hoge = require(‘hoge’) のように、require結果はその都度、ローカルスコープに変数がつくられるので、 require の数だけ 別の参照がつくられることになりますが、一方、ESモジュールの import によってつくられた 変数は、コード全体で同じ参照をもつという仕様なので、import は一度だけ インライン展開すればそれで済みます。この辺りもコード量減につながっているのかもしれません。

rollupの処理の流れ

次に、現時点で rollup がどんな動作をするのかかんたんにまとめてみます。

プラグインなんもいれてない場合

まず、 プラグインなしの場合の rollup がしてくれることは以下です。

  • エントリポイントの js ソースファイルいっこを入力にとる。
  • ソースファイルに import 文 がみつかったら、相対パスからモジュールファイルを探してきて読み込んで展開。1つのファイルにまとめる。
  • もし 対応するモジュールが相対パスから発見できなかったら、無視。(import文がそのままソースに残る
  • その他、オプションによって以下がサポートされている。
    • ソースマップの出力
    • 出力されるjs を、ローカルスコープでラップするか否か、する場合はそのフォーマットの指定。

rollup は、プラグインを使うことで、モジュールを解決する際の検索対象をカスタマイズしたり、各モジュールをトランスパイルしたりといったことができます。

プラグイン: rollup-plugin-node-resolve

  • rollup-plugin-node-resolve を入れると、 node_modules/ ディレクトリ内のパッケージもモジュールの検索対象に含んでくれます。
  • デフォルトでは、 node_modules/hoge などモジュールのルートディレクトリが対象になります。
    • たとえば、 import { a } from ‘hoge’ とした場合、 node_modules/hoge/a.js が対象に。
    • jsnext: true を 設定しておくと、 nodeモジュールの package.jsonjsnext プロパティが設定されているか見に行き、そこに指定されているファイル名をモジュールデフォルトとして扱います。

プラグイン: rollup-plugin-commonjs

  • rollup-plugin-commonjs を入れると、CJS モジュールでの提供しかない npm パッケージ を rollup で読み込めるようになります。
  • 具体的には、 npm モジュール内の 、CJS 記法、exports とかを、 ES の export 文に トランスパイルする、ということをやっているみたいです。
  • この発想、興味深いです。
  • また このプラグインは、 副次的効果として、 index.js という名前のファイルを特別扱いし、 フォルダ内のデフォルトとして扱うようになるという、CJS 由来の 仕様 もサポートされます。
  • 僕は、CJS モジュールでしか提供されていない npm パッケージを明示的に指定して使っています。

プラグイン: rollup-plugin-babel

最終的に ブラウザで動作させる js にするには、まだ実装されていない仕様を 変換 するという工程が必要です。

rollup-plugin-babel を使うことで、各モジュールを1つ1つ babel で変換します。

babel は、サポートしたいES の仕様をpluginによってフレキシブルに指定できるようになっていますが、rollup から使う場合は、 「ESモジュールを CJS モジュールに変換する」というplugin を除外しないといけません。 rollup は ESモジュールをそのまま解釈し、CJSを解釈できないので。

そのため、公式にもあるとおり、 babel-preset-es2015-rollup という preset を指定します。これは、 babel-preset-es2015 から、 babel-plugin-transform-es2015-modules-commonjs を取り除いた ものみたいです。

他にも、rollup の作者による ES の トランスパイラ、buble というものもあり、rollup-plugin-buble というのもあります。

だいたいこのあたりのプラグインを入れておけば、やりたいことが実現できると思います。 他、watch や uglify などのプラグインもあります。