Advent Calendarの何日目かです。近年、JavaScriptでプログラムを書くのに避けて通れない「プリコンパイル」について、Riotの文脈で3本ほど書いてみたいシリーズの2本目です
- ES6で書くRiot - riot.config.js編 - (サンプルコード)
- ES6で書くRiot - Rollup編 - (サンプルコード)
- ES6で書くRiot - Felt編 (予定)
バンドラ? トランスパイラ?
riot.config.js編では、riot
コマンド経由でトランスパイルする方法を扱いました。具体的なツールとしては、babelやbubléが「トランスパイラ」にあたります。要は、この手の(↓)変換です。
- 変換前:
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()
としていたところ、次のようにstyle
とparsers
を指定します。
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
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界隈だと信じています。