Rollupがちょうどいい感じ

  • 236
    Like
  • 3
    Comment

昨年の途中からちらほら耳にするものの、まだ「なにそれ美味しいの?」なRollupですが、馴染むと手放せなくなる感じ。どんなものか、使い方から、プラグインのつくりかたまで、概観してみたいと思います。

1453371698-C1EEDF88-C74B-4FC7-8CF9-5FB27AF2A58D.png

Rollupって何?

複数ファイルに書かれたJavaScriptを、モジュールなどを読み込みつつ、ひとつのバンドルにしてくれるツール。WebPackとかBrowserifyみたいなやつです。依存モジュールの解決や、AltJSのプリコンパイルしたり、など。大きな特徴として、次の点がよく挙げられます。

  • 生成ファイルが小さい
  • ES6(ES2015)ネイティブ

ドキュメント類はまだ最低限という感じですが、WebPackかBrowserifyにさわったことがあれば、そんなに迷うことはないかも。ただ、トップページに行っても正直よくわからないので、今のところWikiが一番の情報源です。公式の情報で見るべきところを、以下ピックアップしておきます。

ES6ネイティブ

BrowserifyはCommonJSで書かれたブラウザ向けコードを、Nodeの作法でコンパイルしてブラウザーで使えるようにするというものでした。WebPackはさらにCSS、画像などJavaScriptに限らずさまざまなファイルをまとめて変換してくれますが、いずれもES5を前提にしている点は共通です。たとえばCoffeeであれば、

  • CoffeeScript --> ES5 --> CommonJS / AMD

という変換を経ます。一方でRollupは、一旦なんでもかんでもES6にして、あとは必要に応じてbabelでES5に変換します。内部処理は、常にES6を前提に行われます。

  • CoffeeScript --> ES6 --(babel)--> ES5 --> CommonJS / AMD

実際のところ、npmに公開されているCommonJSのモジュールはそのままだと読み込むことすらできません...!! rollup-plugin-commonjsプラグインで、ES6に変換してから結合してやる必要があります。

  • CommonJSモジュール(ES5) --(プラグイン)--> ES6 --(babel)--> ES5 --> CommonJS / AMD

Tree Shaking

ES5からES6へ、さらにES5に戻すなんて「無駄!」と思うかもしれませんが、ES6中心にすることにはメリットがあります。その一つがコードの「静的解析」です。両者でのコードのインポートを考えてみてください。

  • CommonJSスタイル: const something = require('something')
  • ES6スタイル: import { foo } from 'something'

ここだけだと似てはいますが、CommonJSはあくまでもJavaScript(ES5)なので、コード内のどこにでも書くことができてしまいます。またrequire()に渡す内容は変数でも良いため、どんなライブラリが使われるかも事前には決定しません。これは、コードの最適化の観点からは、辛いこと尽くし...。実際には膨大なライブラリ群のごく一部しか利用していなくても、全体を読み込む必要が生じます。

その点、ES6のスタイルは、コンパイル(あるいは読み込み)の時点で、依存ライブラリのどの部分が実際に使われるかが「決定」しています。つまり、不要部分はコンパイルの時点で除去してしまっても構わないのです。また、お互いの関数がグローバル汚染していないのであれば、無駄にクロージャで囲む必要もありません。Tree Shakingは「静的解析」をコードにかけることで、バンドルサイズを非常に小さく抑えてくれます。

補足: 近々リリースされる、WebPack2にも同様の機能が実装されるようです。

使い方

gulp他から使うこともできますが、ここではコマンドラインから利用する方法と、JavaScript APIから実行する方法を紹介します。

インストール

何はともあれインストール。ひとまずローカルに。

$ npm i -D rollup

本体のほか、プラグインもまとめてインストールしましょう。定番はこのあたり。

  • rollup-plugin-node-resolve
  • rollup-plugin-commonjs
  • rollup-plugin-json

とBabel関連。後者はプラグインではないですが、バベるときに必要なので一緒に。

  • rollup-plugin-babel
  • babel-preset-es2015-rollup
$ npm i -D rollup-plugin-node-resolve rollup-plugin-commonjs rollup-plugin-json rollup-plugin-babel babel-preset-es2015-rollup

.babelrcを置いてrollup用のプリセットを指定します。

{
  "presets": [ "es2015-rollup" ]
}

これで下準備は完了。(1)コンフィグファイル編か、(2)JavaScript API編かお好きな方をどぞ。

(1) コンフィグファイル編

たぶん、これが一番簡単な方法で、8割がたのケースではこれでOK。

こんな感じで書きます。ES6です。

// rollup.config.js
import nodeResolve  from 'rollup-plugin-node-resolve'
import commonjs     from 'rollup-plugin-commonjs'
import babel        from 'rollup-plugin-babel'

export default {
  entry: 'src/main.js',
  dest: 'dist/bundle.js',
  plugins: [
    nodeResolve({ jsnext: true }), // npmモジュールを`node_modules`から読み込む
    commonjs(), // CommonJSモジュールをES6に変換
    babel() // ES5に変換
  ]
}

  • entry: エントリーポイント
  • dest: ファイルの出力先
  • plugins: 利用するプラグインを指定

プラグインのオプションは、オブジェクトとして渡すことができます。npmプラグインには次のオプションを指定可能で、読み込むモジュール内のpackage.jsonのどこを参照するかが変わります。

  • jsnext: jsnext:mainを参照
  • main: mainを参照
  • browser: browserを参照

package.jsonのscriptsセクションに、実行コマンド$ rollup -cを書いておきます。

{
  "scripts": {
    "build": "rollup -c"
  }
}

実行は、次のコマンドで。これで、コンパイル済みのコードが出力されるはずです。

$ npm run build

メモ1: rollupをグローバルにインストールした場合は、もちろん$ rollup -cで直接実行可。
メモ2: rollup.config.js以外のファイル名にした場合は、$ rollup -c <ファイル名>で指定可。

(2) JavaScript API編

細かくコントロールしたい場合はこちらがお勧め。ライブラリを作る場合、ES6やAMDなど複数の形式に対応したファイルを出力する必要があったり、いろいろ調整が必要だったりするのでこの方法を使っています。
詳細はコメントを参照のこと。ES6で書いていますが、素のNodeだとimportとかは使えないので注意。

// build.js
const
  rollup   = require('rollup'),
  npm      = require('rollup-plugin-npm'),
  commonjs = require('rollup-plugin-commonjs'),
  babel    = require('rollup-plugin-babel'),
  name     = 'awesomeapp'

rollup
  .rollup({
    // rollup.config.jsと同じようにentryやpluginを指定
    entry: 'dist/main.js',
    plugins: [npm({ jsnext: true }), commonjs(), babel()]
  })
  .then(bundle => {
    // ES6形式で出力
    bundle.write({ format: 'es6', dest: `dist/${ name }.es6.js` })
    // AMD形式で出力
    bundle.write({ format: 'amd', dest: `dist/${ name }.amd.js` })
    // CommonJSで出力
    bundle.write({ format: 'cjs', dest: `dist/${ name }.cjs.js` })
    // グローバル変数を使う形式で出力
    bundle.write({
      format: 'iife',
      dest: `dist/${ name }.js`,
      moduleName: name // iifeで出力する場合は、moduleNameの指定が必須
    })
  })
  .catch(error => {
    console.error(error)
  })

write()の代わりにgenerate()を使うと、文字列として返します。

  • bundle.write(): 上記のようにファイルに出力
  • bundle.generate(): ファイルに出力せず、文字列を返す

あとは、package.jsonのscriptsセクションに、実行コマンドを書いておきます。

{
  "scripts": {
    "build": "node build.js"
  }
}

実行は、次のコマンドで。

$ npm run build

若干の補足を

説明しきれていない項目を、2つほど。その他については、Wiki参照。

jsnext:main

最近、package.jsonにときどき見かけるアレです。mainに指定されたコードは基本require()形式で書かれていることが想定されるので、ES6のまま書く場合は不適当となってしまいます。そこで、ES6で書かれていることを明示したい場合は、jsnext:mainを使うことが提唱されています

external

例えば、import riot from 'riot'がコードに含まれていても、riotモジュールだけ展開せずにおきたい場合があります。その場合は、externalオプションで除外モジュールを指定します。例:

{
  entry: 'src/main.js',
  external: ['riot'],
  plugins: [...]
}

まとめ

次期HTML仕様に、2016/1/21付で待望の「モジュール」が登場しました! 今後はブラウザがネイティブでモジュールローダを備えるようになると期待します。とは言え、まだしばらくは時間がかかるでしょう。

つまり、それまではなんらかの方法でコードをバンドルする必要があります。従来、有力なのはBrowserifyとWebPackでしたが、この数ヶ月でRollupもその一角を担い始めました。筆者自身は、gulp + Browserifyで長らくやってきましたが、ES6を扱うのがしっくりこないんですよね...。WebPackは"Do One Thing and Do It Well"じゃない感じが苦手。もし、そんな同じような悩みを持っていたら、Rollupは気にいるかもしれません :-)

  • Browserify: APIに少々クセあり・コンパイルに時間がかかる
  • WebPack: ちょっと色々できすぎるのが難・Code splitは魅力的
  • Rollup: シンプルでちょうどいい感じ

Appendix:

プラグイン

RollupのWikiにプラグインが掲載されています。2016年1月時点のものを転載してみます。下記のほか、npmにはすでに30種類ほどが公開されているようです。

  • babel – バベる
  • coffee-script – CoffeeScriptを変換
  • commonjs – CommonJSをES6モジュールに
  • inject – 依存性を検知してインジェクト
  • json – JSONの取り込み
  • memory - ファイルからではなく、文字列でソースを渡す
  • multi-entry – 複数のエントリーポイントを扱う
  • npm – npmでnode_modulesにインストールされたものを扱えるようにする
  • pegjs - PEG.jsの文法定義ファイルを変換
  • postcss - PostCSS変換とheadへの挿入
  • replace – 文字列置換
  • riot - Riot.jsのタグファイルを変換 (拙作)
  • string – テキストファイルを文字列としてインポート
  • uglify - Uglifyでバンドル結果をミニファイ

プラグインの作り方

もし、ソースを問答無用で全部.toUpperCase()するプラグインを作るとすると、こんな感じです(使い道ないけど)。optionsを受け取って、transform()を含むオブジェクトを返すという作法になっているのが見て取れると思います。

import { createFilter } from 'rollup-pluginutils'

export default function uperCase(options = {}) {
  // options.include(=含める), options.exclude(=除外する)は対応が必須
  const filter = createFilter(options.include, options.exclude)
  return {
    transform (code, id) {
      if (!filter(id)) return null // ファイルパスでフィルターする
      return code.toUpperCase() // 変換したコードを返す
    }
  }
}

.toUpperCase()する代わりに、お好きなAltJSコンパイラに通せば、あれやこれやできそうですね。このまま、自分のrollup.config.jsなどから呼び出せば、プライベートなプラグインとして使用可能です。

プラグインの公開

プラグインを公開する場合は、「モジュール名をrollup-plugin-で始める」「rollup-pluginキーワードをつける」などのガイドラインが決まっています。目を通しておきましょう。