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

Webpackerを使ったRailsでのJavaScript開発

こんにちは。マーケティングプロダクト開発部の長田です。

この記事では、私が現在進めているプロジェクトで、Webpackerを使ったJavaScriptのモジュール管理を導入したので、それについて紹介したいと思います。

Webpackerとは

Webpackerとは、Webpackを用いてRails上でJavaScript開発をするために必要な一連の流れを提供してくれる、Rails organizationで開発されているgemです。

これまで、Rails上でJavaScriptのパッケージをどのように管理するか、また、モジュール依存をどのように解決するかについて、多くの選択肢があり、それらをどう組み合わせて使うのかについて悩まされてきました。 このブログでも過去に何度か記事が投稿されており、その中でも複数の選択肢が上げられています。

私のプロジェクトでは、ユーザーが入力するフォームが多く、「リッチなフォームで使いやすい入稿システムを作りたい」という要件があったので、JavaScriptのパッケージをいくつか組み合わせて作ろうと思っていて、Yarn、Webpackを使って実現しようとしたのですが、考えることが多いなという印象でした。*1

このような問題に対してWebpackerを使うと、ある程度Railsの開発プロセスにのったJavaScriptの開発ができるのではないかと思い、これを機に導入を試みました。

導入

Webpackerのセットアップ方法や使い方等は README に載っているので、そこを参考にしつつ進めることで導入できました。

以下では、導入するにあたって検討したことについて、主に次の2点について紹介します。

  • JavaScriptファイルの配置の方針
  • フロントエンドのテスト

JavaScriptファイルの配置の方針

導入にあたって、悩んだのはディレクトリ構成をどうするかでした。

ポイントは以下の2点です。

  • app/assets/javascriptsの扱い
  • エントリーポイントとしてビルドするファイル

app/assets/javascriptsの扱い

Webpackerでモジュールを管理できるようになったとはいえ、RailsはSprocketsを捨てる訳ではないらしいので、viewごとのアセットは今まで通り、app/assets/javascripts以下に置いても良さそうなのですが、個人的には、モジュールの管理がプロジェクト内で分散するのは避けたかったため、全てのファイルをapp/javascripts以下に置くことにしました。

幸い、私のプロジェクトはスタートしたばかりで、ファイルが少なかったので、app/assetsにあるものを難なく移行するとこができました。

エントリーポイントとしてビルドするファイル

現時点で、 rails webpacker:install で生成されるWebpackの設定では、app/javascripts/packs直下に配置したファイルがそれぞれエントリーポイントとしてビルドされるような設定になっています。

私のプロジェクトではapp/assets/javascriptsに置いていたファイルはSprocketsで1つのファイルにまとめていたので、それと同じようにするには、app/javascripts/packs以下にapplication.jsだけを配置して、そこから、他のモジュールを読み込むようにする必要がありました。

しかし、以下の点から複数のエントリーポイントを作るようにしました。

  • JavaScriptが必要なviewごとにview modelを作る方針*2
  • 全てのviewにview modelがあるわけではない
    • むしろ少ない

という状態だったため、view modelごとにエントリーポイントを作り、必要なviewからjavascript_pack_tagで読み込むようにしました。

これにはメリット・デメリットはありますが、メリットとして大きいと思うのは、

  • JavaScriptの挙動の影響範囲を特定しやすくなる

という部分です。

1つのファイルにまとまっている場合、コードベースが大きくなると、ロジックがどこで定義されているのか、また、ロジックがどこに影響するのかの見通しが悪くなってしまいがちです。特にViewごとにJavaScriptを書いていて、それが1つのファイルにまとまっていた時、ファイルをまたいでDOMのidやclassが被ってしまうと予期せぬ挙動を与えてしまうこともありえると思います。

なので、複数のエントリーポイントを作り、必要なviewごとに読み込む方針は上のような問題に対しては有効だと思いました。

一方で、デメリットとして大きい思うのは、

  • エントリーポイントごとに、パッケージがバンドルされ、ファイルサイズが大きくなりがち

というのが挙げられますが、

  • エントリーポイントに含まれるパッケージが、まだそれほど大きくない
  • Webpackの設定で、共通のパッケージだけ別でビルドして最適化できる

という点から、デメリットは顕著に現れないだろうと考え、packs以下にview modelを配置して、それぞれエントリーポイントとしてビルドする方針にしました。*3

フロントエンドのテスト

フロントエンドのテストを書く手段として、

  • E2Eテストで、ブラウザの振る舞いをテストする
  • JavaScriptのモジュール単位でテストをする

2つの選択肢があると思うのですが、私のプロジェクトでは、前者を採用することにしました。理由は、

  • フロントエンドにロジックを持っているとはいえ、そこまで複雑なロジックではない
  • サーバーサイドのテストと同じフレームワークで書けるので、導入コストがほとんどない

という点から、E2EテストでJavaScriptで書いたロジックのテストをすることにしました。

そこで問題になったのは、モジュールのビルドをどのタイミングで行うかです。

いわゆるRails wayなプロジェクトだと、Sprocketsをつかってモジュール管理をすると思うのですが、Sprocketsはアセットのリクエスト時にビルドをしてくれるので、phantomjsなどのヘッドレスブラウザを使ってテストをすることで、アセットのビルドを気にすることなくE2Eテストが書けました。

しかし、Webpackerを使う場合は、テスト実行前にモジュールをビルドする必要があり、その設定とタイミングで悩みました。

私のプロジェクトでは、test環境のビルド設定をdevelopment環境に合わせ、テストフレームワークのフック等とは別で、あらかじめアセットをビルドする方針にしました。*4

ビルドの設定をdevelopment環境と合わせることで、開発中でもプロジェクトのビルド設定(RAILS_ENV的なもの)を変更することなくテストを実行できます。 また、ビルドのステップをテストとは別にすることで、開発中には不要なビルドが走らず、こまめにテストを実行することができます。

現時点で、rails webpacker:install で生成されるWebpackの設定は、test環境のものは生成されないので、config/webpack/test.jsを追加しました。

module.exports = require('./development.js');

こうすることで、RAILS_ENV=testの場合でも、bin/webpack を実行することでdevelopment環境と同じようにビルドすることができるので、CIで以下のようなスクリプトを書いても、開発中と同じようにテストをすることができます。

export RAILS_ENV=test
bin/webpack
# additional pre process
# test

まとめ

この記事では、RailsアプリケーションでWebpackerを使ったJavaScriptの開発について書きました。 Yarn、Webpackを使ったモダンなJavaScriptの開発環境が、rakeタスクなどが用意されている状態で、シュッと整うのは非常にいいと感じました。

ただ一方で、packs以下に配置するファイルの方針を考えたり、デフォルトのWebpackの設定だとディレクトリ構成に制約があったり、test用の環境はそれぞれでセットアップしなければいけなかったりするのと、今回の記事では触れませんでしたが、現時点ではCSSのビルドに対応できていない等、課題はまだまだありそうだなという感触です。

最後になりましたが、この記事がRailsでのJavaScriptの開発について、少しでもお役に立てれば幸いです。

*1:ビルド成果物をアセットパイプラインにのせるのかどうか、テストやデプロイ、digest等をどうするかを考えなければいけないので、多いなと思いました。

*2:私のプロジェクトでは、Vue.jsを導入しており、ここでview modelと呼んでいるのは、root vue instanceにあたります。

*3:ただし、しばらくWebpackerを使っていくうちに、不都合が出てきたときを想定して、Sprocketsに切りもどせるようにコードを書くよう意識しています。

*4:私のプロジェクトではRSpecを使っており、ここでいうフックとは、before(:suite)等のことを指します。

/* */ @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;*/ /*}*/