ES6で書くRiot - Rollup編


Advent Calendarの何日目かです。近年、JavaScriptでプログラムを書くのに避けて通れない「プリコンパイル」について、Riotの文脈で3本ほど書いてみたいシリーズの2本目です

バンドラ? トランスパイラ?

riot.config.js編では、riotコマンド経由でトランスパイルする方法を扱いました。具体的なツールとしては、babelbubléが「トランスパイラ」にあたります。要は、この手の(↓)変換です。

  • 変換前: this.message = `Hello ${name}!`
  • 変換後: this.message = "Hello " + name + "!"

ただ、タグからimport文を使って他のライブラリを参照しようとすると、良い方法がありません。そこで必要となるのがバンドラです。シンプルな話としては、トランスパイラの前段に置かれて複数のJavaScriptをまとめる役目を果たします。

  • 変換前:
    • import { add } from './a'; console.log(add(1, 2))
    • export add function add (a, b) { return a + b }
  • 変換後:
    • function add (a, b) { return a + b }; console.log(add(1, 2))

これも絵にすると、こんな感じでしょうか。

ただ、実際にはもう少し複雑で、世に公開されているツールの多くはまだブラウザ(ES6)ではなくNode.js(CommonJS)向けに書かれています。それらのツールを使うには、CommonJS→ES6変換が必要です。

本稿の本題である「Riotのタグを一緒に読み込む」場合も、Riotコンパイラを事前に通してJavaScriptに変換しておく必要があります。

Rollup

さて、いよいよRollupです。このツールを使えば、複数のファイル、さらにはnpmで公開されているモジュールたちを、ひとつのファイルにまとめられます。これには、フロントエンドにおいて、3つの大きなメリットがあります。

  • 機能ごとにファイルを分けられる
  • 先人の作ったライブラリに乗っかれる
  • 1ファイルにまとめてリクエスト数と帯域を圧縮

ツールそのものについては、以前に書いた記事もあるので、そちらも参考にしてください。

メモ: バンドラとしての選択肢は3つほどあります。

  • webpack: 利用例多し。多機能すぎ。HMR(hot module loading)が魅力
  • Rollup: シンプル、高速、生成コードが小さい
  • Browserify: 新規プロジェクトではもう使わない

好みで言えば圧倒的にRollup推しですが、周りの環境(と人)に合わせて選択して幸せになりましょう。

準備

Riotの公式Examplesに、Rollupの例も用意しました。これを使いましょう。次のコマンドで、クローンしてきて該当ディレクトリに進みます。

$ git clone https://github.com/riot/examples
$ cd example/rollup

メモ: まだ、Gitをインストールしていない場合は、ここからダウンロードするのでも構いません。

ファイル構成は次の通りです。

  • src/
    • main.js - JavaScriptのエントリーポイント
    • app.tag - メインビューのタグ
    • md.tag - Markdown表示のためのタグ
  • index.html
  • package.json
  • rollup.config.js - Rollupの設定ファイル

設定ファイル

上記それぞれのファイル内容は、適宜調べてもらうとして、ここで重要なのはrollup.config.jsです。このうちの、基本部分のみを抜き出すと次のようになります。

// Riotプラグイン
import riot from 'rollup-plugin-riot'
// ES6トランスパイラ
import buble from 'rollup-plugin-buble' 

export default {
  entry: 'src/main.js', // JavaScriptのエントリーポイント
  dest: 'dist/bundle.js', // 出力先
  plugins: [
    // 以下、プラグイン設定
    riot(),
    buble()
  ],
  format: 'iife' // えーっと、とりあえずこう書いてください
}

順番に見ていけば、それほど難しいことはなくて、

  • プラグインを読み込み
  • 入力ファイルと出力ファイルを設定
  • プラグインを適用

をしているだけです。

実際には、Nodeのライブラリを使うため、もう二つほどプラグインが必要です。こうなります。↓

// Riotプラグイン
import riot from 'rollup-plugin-riot'
// node_modulesの中から取ってくるためのプラグイン
import nodeResolve from 'rollup-plugin-node-resolve'
// さきほどの図の中の「CommonJSブリッジ」に相当
import commonjs from 'rollup-plugin-commonjs'
// ES6トランスパイラ
import buble from 'rollup-plugin-buble' 

export default {
  entry: 'src/main.js', // JavaScriptのエントリーポイント
  dest: 'dist/bundle.js', // 出力先
  plugins: [
    // 以下、プラグイン設定
    riot(),
    nodeResolve({ jsnext: true }),
    commonjs(),
    buble()
  ],
  format: 'iife' // えーっと、とりあえずこう書いてください
}

実行

では、依存モジュールをインストールして、実際に実行してみましょう。

$ npm install
$ npm run build

distフォルダに、bundle.jsが生成されているはずです。確認してみてください。ちょっとごちゃっとして見えるかもしれませんが、

  • main.js
  • app.tag
  • md.tag
  • riot.js
  • marked.js

の5つ(+依存ファイル)がひとつのコードにまとまったのが見て取れると思います。

メモ: 上記、npm run buildにはrollup -cコマンドが割り当てられています。このように、npmスクリプトに書いておけば、rollupをグローバル環境に入れる必要はありません。
複数人で作業している場合も、npm installするだけで全員の環境を揃えられるのは大きなメリットです。

タグ内のCSSどうしよう問題

上記までで済めば、比較的シンプルだったのですが、最後に浮上するのがCSSのプリコンパイルです。ここまで、JavaScriptの文脈で説明してきましたが、同じようなことはCSSでも起きています。

  • JavaScript: 次世代標準(ES6・ES7)を現在のブラウザへ
  • CSS: 次世代標準(CSS3・CSS4)を現在のブラウザへ

CSSの文脈で使われるのはPostCSSで、そのプラグインを束ねて使いやすくしたものがcssnextです。これを使えば、自動でベンダープリフィックスをつけたり、CSS変数を使ったりが可能になります。

どう組み込む?

単体のCSSであれば、cssnext単体で簡単に変換できます。でも、思い出してください。Riotではタグファイルの中に一緒にCSSも書くことで、コンポーネントのメンテナンス性を高めているのでした。...ということは、Riotコンパイラの中でcssnextを実行する必要があります。絵にするとこうですね。先ほどの図より詳しくなっているRiotコンパイラ周辺に注目してください。

Riotのオプションにカスタムパーサを入れる

そのための方法がRiotにも用意されています。(注)

Riotでは、HTML, JavaScript, CSSに関して、それぞれカスタムパーサを設定できます。Rollupから利用する場合、先ほどはプラグイン設定でシンプルにriot()としていたところ、次のようにstyleparsersを指定します。

riot({
  style: 'cssnext',
  parsers: {
    css: { cssnext }
  }
})

上記、cssnextは別途次のように定義しています。:scopeをうまく使うために、少しトリックを入れていますが、基本的にはpostcss([プラグイン]).process(css).cssが全てです。cssnext以外のプラグインを使いたい場合は、配列に足しましょう。

function cssnext (tagName, css) {
  // ちょっとだけハックして、:scopeを:rootに置き換えてPostCSSに渡す
  // タグの中でCSS変数を使いやすくするため
  css = css.replace(/:scope/g, ':root')
  css = postcss([postcssCssnext]).process(css).css
  css = css.replace(/:root/g, ':scope')
  return css
}

: 実は、本稿のために(ということもないのですが)、これを実現するコミットをRollupプラグインに数日前入れました。

まとめ

最終的に、全体像としてはこのようになりました。いろいろ説明した割には短いですね w

rollup.config.js
import riot from 'rollup-plugin-riot'
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import buble from 'rollup-plugin-buble'
import postcss from 'postcss'
import postcssCssnext from 'postcss-cssnext'

export default {
  entry: 'src/main.js',
  dest: 'dist/bundle.js',
  plugins: [
    riot({
      style: 'cssnext',
      parsers: {
        css: { cssnext }
      }
    }),
    nodeResolve({ jsnext: true }),
    commonjs(),
    buble()
  ],
  format: 'iife'
}

function cssnext (tagName, css) {
  css = css.replace(/:scope/g, ':root')
  css = postcss([postcssCssnext]).process(css).css
  css = css.replace(/:root/g, ':scope')
  return css
}

現在のところ、もっとも柔軟性と拡張性が高く、どっちにでも転べる(?)書き方になったかなと思います。というわけで、RiotとRollup使おうよ!

APPENDIX: CommonJSなのかES6なのか

結論から書くと、Riotでタグを書くときはES6の記法を使えばOKです。CommonJSのモジュール記法を使う理由は見当たりません。記法については、記事がたくさんあるので、ここでは見分け方だけ。

  • ES6モジュール: importとかexportがあったらES6モジュール
  • CommonJS: requireとかmodule.exportsがあったらCommonJSモジュール

CommonJSは、ES6以前に提唱されていたモジュール読み込みの仕組みで、JavaScriptの文法そのものは拡張せずrequire()という関数とexportsという変数だけでどうにかしていたのが画期的でした。ただ、ES6で文法側で「モジュール」の仕組みが加わったことで、事態が複雑化しています。一部、CommonJSでてきていたことがES6で出来なかったり、その逆があったりと「相互運用」も課題です。

メモ: 両方が「JavaScript」と言っているので初見殺し感があります。

フロントエンドで書く場合、

  • 自分のコードはES6で書く
  • ES6版のライブラリがない場合は、CommonJSのモジュールをブリッジ経由で使う

を基本としましょう。

メモ: Node.js用に書かれた資産が膨大なので、一朝一夕にすべてES6に変わる...ということはないですが、全体としてはじわじわとES6に軸足を移しているのが今のNode.js界隈だと信じています。