読者です 読者をやめる 読者になる 読者になる

モダンJavaScript開発環境 on Rails

投稿推進部の外村(@hokaccha)です。

クックパッドブログの開発でRails上にECMAScript6などのモダンなJavaScript開発環境を導入した経験を元にノウハウを紹介したいと思います。

RailsはSprocketsというgemでJavaScriptやCSSをコンパイルする仕組みが提供されています。Sprocketsによるasset管理の仕組みは非常によくできており、AltJSのトランスパイルやファイルの結合、minifyなど、assetのコンパイルに必要な機能を一通り備えています。

しかし、JavaScriptにおけるモジュールの依存関係の解決や、ライブラリの管理などについてはモダンなJavaScript開発と乖離してきているのが現状です。そこで、Railsでも以下のようなことを実現できることを目標に環境を作りました。

  • ECMAScript6のシンタックスを使う
  • モジュールの依存解決にCommonJS Modules(もしくはES6 Modules)を使う
  • フロントエンドのライブラリ管理にnpmを使う

これらを実現するためにフロントエンドの開発ではスタンダードになってきているbrowserifyBabelといったツールを使うことにしました。これらのツールをRails上でどのように利用するかというのが今回の話です。

gulpを使う

まずひとつめの方法として考えられるのは、Railsのassetのコンパイルの仕組みを完全に捨て、フロントエンドの開発でよく使われているgulpなどのタスクランナーを利用し、その中でbrowserifyやその他のプラグインを使ってassetのビルドをおこなう方法です。

この方法はRailsのレールから完全に外れるため自由度が高いという利点がある一方で、環境構築にかかるコストがそれなりに高いというのが欠点です。

RailsのアプリケーションはAPIだけを提供し、サーバーサイドとクライアントサイドを完全に分けて開発できるケースや、フロントエンドに詳しいエンジニアがいて常に最新の環境に追従できる場合などはこの手法を取るメリットがあると思います。

一方で、ある程度Railsのassetのビルドの仕組みを利用しつつJavaScriptのビルドだけbrowserifyを利用したいという場合には少々オーバースペックです。CSS(Sass)のコンパイルや、デプロイ時のminify、キャッシュ対策のdigest値の付与など、Railsの機能でまかなえる部分はRailsに乗ってしまったほうが楽です。

また、そこまで多くありませんが、JavaScriptのライブラリがgemとしてしか提供されていないもの*1もあるため、そのようなライブラリを読み込む際に既存のSprocketsの//= requireを併用したいというケースもあります。

gulpでコンパイルしたものをRailsから読み込むためのgulp-rev-rails-manifestや、gulpでSprocketsと同様の機能を提供するgulp-sprocketsなどを利用してRailsと併用するという手段もありますが、今回はできるだけRailsに寄せて環境を作りたかったため、こういった手法の採用は見送りました。

browserifyを使う

そこで今回はまず、browserifyを直接利用し、それ以外はRailsに任せるという方法を採用しました。まず、次のようにbrowserifyで対象のファイルをビルドし、成果物をapp/assets/javascripts以下に出力します。

$ browserify -t babelify app/assets/javascripts/src/main.js -o app/assets/javascripts/bundle.js

babelifyというのはbrowserifyのビルド過程でBabelによる変換を行ってくれるbrowserifyのプラグイン*2です。

このとき、元のソースファイル(ここではmain.js)はapp/assets/javascripts以下でなくてもどこにあっても構いません。重要なのはbundle.jsをSprocketsのload path以下に出力することです。そうすることでapplication.js等から以下のように生成したファイルを呼び出せます。

//= require bundle.js

こうすることで、ECMAScript6へのトランスパイルやモジュールの依存関係の解決などはbrowserifyに任せつつ、Sprocketsとの併用ができますし、デプロイ時もassets:precompileの前にbrowserifyのビルドコマンドを実行するだけで済みます。

実際の開発時にはwatchifyという対象のJavaScriptファイルの変更を監視して、変更があったときにbrowserifyの差分ビルドを行ってくれるツールを利用していました。環境やコード量にもよりますが、browserifyは単体で実行すると10秒近くビルドに時間がかかる場合もありますが、watchifyの差分ビルドだと1秒弱ぐらいまで高速化されます。

この方法はある程度うまくいっていたのですが、JavaScriptのファイルを変更してすぐにブラウザをリロードしたときに、ビルド途中の中途半端な状態でSprocketsのほうにキャッシュにされ、再度JavaScriptのファイルを何かしら更新してビルドし直さないとバグったままになってしまうという現象に悩まされました。そんなに頻繁に発生するわけではないのですが、一日開発していたら数回は遭遇するのでけっこうなストレスです*3

また、ビルドされたファイルはバージョン管理システムの管理には含めないので、JavaScriptの開発を行わないエンジニアやデザイナがアプリケーションの開発を行うとき、手元でビルドプロセスを走らせる必要があります。そこまで大きな問題ではないですが、できればこれまで通りrails serverを立ち上げるだけで開発できるようにしたほうがよいと考え、次で説明するbrowserify-railsを導入することにしました。

browserify-railsを使う

browserify-railsはその名の通り、Railsでbrowserifyを使うためのgemです。現状はひとまずこの方法に落ち着いています。

https://github.com/browserify-rails/browserify-rails

Sprocketsのプラグインになっており、Sprocketsのビルドプロセスの中でbrowserifyが実行されます(正確にはSprocketsのPost Proceccerとして動作します)。

つまり、ブラウザのリロードをしたタイミングでbrowserifyのビルドが走るためrails server以外に別プロセスを起動するといったことが不要になりますし、ビルドのタイムラグでタイミングによってはJavaScriptが更新されないという問題もなくなります。また、当然Sprocketsと併用でき、Railsが提供しているassetの仕組みをそのまま使うことができます。

browserify-railsの最大の問題点はビルドの速度です。browserify-incrementalというツールを使うため、browserifyをそのまま使うよりは多少速いですが、watchifyほどの速度はでません。JavaScriptを更新してブラウザをリロードすると数秒待ち時間が発生します。

browserify-railsのドキュメントにあるように、browserifyのMultiple bundlesを使ってサイズが大きいライブラリ(React.jsなど)を別ファイルにするという方法もありますが、それでもこれまで通りのレスポンス速度で開発できるほどは速くなりません。(JavaScriptに変更がない場合はキャッシュが効いて即レスポンスが返るのでJavaScriptを変更しない場合の開発の速度には関係ありません)

逆に、速度以外で困ることはほとんどなく、Railsのレールをできるだけ外れずにモダンなJavaScript開発環境が作れるので、browserify-railsのような仕組みを高速化していくというのは一つの方向性としてはよいのではないかと個人的には思っています。

以下に最小限の構成を作ったので興味がある方は試してみてください。

https://github.com/hokaccha/browserify-rails-example

まとめ

RailsでECMAScript6やnpmなどのモダンなJavaScript環境を整えるためのノウハウについて書きました。

フロントエンドの開発環境まわりはまだまだ過渡期なので、これという決定的な方法が確立されているわけではありません。今回紹介したような方法をはじめ、いくつかの選択肢があるのでプロジェクトの規模や好みに合った方法を探してみてください。

*1:Turbolinksなど

*2:正確にはtransform

*3:livereloadのような仕組みをいれることによって解決できる可能性もあるかもしれません

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/