こんにちは、@mugi_uno です。
庭に花壇を作ったところ変に日焼けをしてしまい、半袖で外に出るのが恥ずかしいです。
つい最近、Misocaのフロントエンド周りのビルドをWebpackerを利用したものに置き換えました。 様々な知見が得られたので書いてみたいと思います。
Webpacker?
フロントエンド用のビルドツールであるwebpackをRailsから簡単に使えるようにするためのRubyGemsです。
Rails5.1からはこのWebpacker経由で、rails new
の際に同時にwebpack用の設定ファイルを生成することもできます。
流行りのフロントエンド環境を構築しようとする際、最近ではwebpackを利用するケースが多いですが、実際にはES6やReactなどが混ざることも多く、慣れていないと設定するだけでもなかなか敷居が高いです。
さらに、Railsと組み合わせる場合には、アセットパイプラインとの兼ね合いをどうするか?などといった課題も登場するため、考えるだけで鼻血が出そうになります。
Webpackerを利用することで、その辺りをいい感じに解消してくれるというわけです。
Misocaでの導入理由
browserify-rails めっちゃ時間かかる問題
もともとはアセットのビルドに browserify-rails を利用していましたが、ビルド時間がMisoca内で問題になっていました。キャッシュによる高速化が効いてはいるものの、何かの拍子にフルビルドが走ると数分待たされることも。
CIでも上記の影響を受けるため、開発効率にあまりよろしくない影響を与えていました。
(開発効率が落ちた図)
なんとかしたい
最初はbrowserify-rails自体のチューニングを検討しましたが、webpackでのビルドを一度試してみたところ、そもそもフルビルドでも20秒くらいで終わることがわかりました。
MisocaではRails5.1を導入済みだったのもあり、Webpackerを使って本格的に導入を試みることに。
webpackerがやってくれること
※以下内容は rails@5.1.1
/webpacker@2.0
を前提としています。
大きく分けると以下の3つかと思います。
- rakeタスクの提供
- ボイラープレートの生成
- viewヘルパーの提供
rakeタスクの提供
Webpackerを追加すると、いくつかのrakeタスクが利用可能となります。 特徴的なものを簡単にご紹介します。
webpacker:install
ボイラープレートを生成する。(後述)
webpacker:check_node
/ webpacker:check_yarn
webpackerの実行には Node.jsとYarnが一定バージョン以上である 必要があります。
このタスクで、システムに必要なバージョンのNode.js/Yarnがインストールされているか検証できます。
webpacker:verify_install
webpacker:check_node
/webpacker:check_yarn
などを実行し、Webpackerによるビルドが可能な状態かを検証できます。
ちなみにブログタイトルの「Webpacker is installed 🎉 🍰 」は、このタスク成功時の出力結果です。
webpacker:compile
bin/webpack
を呼び出し、webpackビルドを実行します。
その際に RAILS_ENV
の値を参照し、各環境に応じたビルドを実行してくれます。
また、実行前には自動的に webpacker:verify_install
によるチェックが行われます。
assets:precompile
からのコールについて
以下は assets:precompile
のenhanceで事前タスクとして登録されています。
yarn:install
webpack:compile
これにより、例えば既存のデプロイプロセスなどで assets:precompile
を実行していれば、そこでwebpackビルドが自動的に実行されます。
ただ、ここで実行されるyarn:install
については、デフォルトでは --pure-lockfile
が付与されないので、意図しないバージョンによるビルドが行われてしまう可能性がある点に注意が必要です。
Misocaでは、デフォルトの yarn:install
タスクをオーバライドし、--pure-lockfile
が付与されるように一手間加えています。
ボイラープレートの生成
webpacker:install
を実行すると、webpackに必要な各種ファイルが自動的に生成されます。
- webpack.config.js に相当するファイル群
- ES6ビルド用のbabelrc
- パス設定などを集約したYAMLファイル(
webpacker.yml
) - などなど
また、Misocaでは利用していませんが、一部ライブラリに特化した形でのインストールも可能です。
webpacker:install:react
webpacker:install:vue
- etc..
これにより、ES6+Reactなどを利用したフロントエンド開発を行うための環境を手早く構築することができ、ダイジェスト付きのjs生成なども簡単に行うことができるようになります。
viewヘルパーの提供
Sprocketsの場合は javascript_include_tag
が提供されていますが、同様にWebpackerでは javascript_pack_tag
が提供され、Webpackerによってビルドされたファイルを利用する場合はこちらを利用する必要があります。
(スタイルシート用に stylesheet_pack_tag
もあります。)
導入のために行った作業
方針設定
がむしゃらに始めると迷宮に入っていくかもしれません。今回は「Webpackerの導入」にフォーカスしたいため、以下のような方針にしました。
- jsのみを対象とし、cssは対象としない
- ビルドを通すため以外のコードの書き換えは行わない
assets/javascripts
配下を webpackのentryとする
webpackはビルドの起点をentryとして設定に記述する必要がありますが、デフォルトでは javascript/packs
配下になっています。
そのままだと既存資産のjsファイルすべてを javascript/packs
配下へ移動する必要がありますが、Webpacker導入作業中もプロダクト開発は行われているため、頻繁にコンフリクトが発生することが予想されました。
さすがにつらいので、entryに assets/javascripts
を加えることで、導入完了後にあらためて移動だけを行うことにしました。
shared.js
に以下のようなコードを加えて、entryにObject.assignでマージすることで対応します。
const assetsPath = 'app/assets/javascripts'; const assetsEntry = sync(join('app/assets/javascripts', extensionGlob)).reduce( (map, entry) => { const localMap = map const namespace = relative(join(assetsPath), dirname(entry)) localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry) return localMap }, {} );
//= require
をCommonJS(またはES6 modules)に書き換え
Sprocketsで依存解決をしている場合、jsまたはcoffee内に以下のような記述が存在する可能性があります。
//= require jquery-ui //= require_tree ./lib
これらは、CommonJSの require
や、ES6 modulesの import
/export
を使った形に書き換える必要があります。
require('jquery-ui') require('lib/foo') require('lib/bar') require('lib/baz')
なお、拡張子を省略した場合にうまくロードされない場合、config/webpacker.yml
内の extensions
に追加する必要があります。
依存関係の解決をnpm利用に変更
必須ではありませんが、上記 //= require
でロードされていたライブラリを、npmモジュールを利用するように変更しました。
地道に各ファイルのバージョンを調べ、同バージョンを yarn add
していきます。
ただ、gemが提供しているjsファイルをrequireしていることもあります。その際は、jsのみがnpmモジュールとして提供されていないか確認し、
- 提供されている → そちらを利用するように変更
- 提供されていない → jsファイルのみを抽出して解決するか、フルパスで
require
する。
とした手順を踏んでいきました。
よく利用されているものでいえば、jquery-ujs
などは単体で提供されています。
グローバル参照を解決
値がグローバル定義されているのを前提としているコードがある場合、グローバルに値を明示的に定義しないとアクセス不可となることがあります。(jQuery
/$
などがよくあるケースだと思います。)
今回はグッとこらえてグローバル依存自体は残しました。意見のある方はこちらからどうぞ!
共通でロードするjsファイルがあれば、その先頭に以下のようなコードを足すだけで参照可能となります。
global.$ = global.jQuery = require('jquery');
なおMisocaで導入した際には、さらに以下のような対応も行っています。
- ライブラリ群のみを個別の jsファイルに抽出した別ファイルとした。
ProvidePlugin
を利用した、ライブラリから別ライブラリの参照。
ひたすらviewヘルパを置換
javascript_include_tag
をひたすら javascript_pack_tag
に置き換えていきます。
基本的には置き換えるだけで問題ないですが、javascript_include_tag
の場合は複数スクリプトを単一タグでロードできるのに対し、javascript_pack_tag
の場合はエラーとなるため、そこだけは分割する必要があります。
<%= javascript_include_tag('foo', 'bar', 'baz')%>
↓
<%= javascript_pack_tag('foo')%> <%= javascript_pack_tag('bar')%> <%= javascript_pack_tag('baz')%>
foremanによる起動設定
開発時には、jsファイルを監視した上で自動ビルドしたくなりますが、その場合、rails serverとwebpackで二つのプロセスの起動が必要です。
単純にターミナルを2つ開くなどして起動してもいいですが、READMEには foremanを利用した方法が記載されています。
こちらも併せて導入しておきました。
(基本的な導入手順はREADMEの記述のままなのでここでは割愛します。)
インクリメンタルビルドについて
webpackを利用したインクリメンタルビルドには2つの方法があります。
bin/webpack-dev-server
bin/webpack --watch
bin/webpack-dev-server
を利用した場合にはホットリロードが有効になるなど、動作に違いがありますが、
好みや環境によってどちらを利用するか選択すれば良いと思います。
キャッシュを有効にしたいなどの理由から、Misocaでは bin/webpack --watch
をデフォルトとし、foremanで利用するProcfileに記載しています。
ちなみに、webpack-dev-server
では待ち受けポートがrails serverとは別(デフォルトでは8080)となりますが、このあたりは bin/webpack-dev-server
や webpack用の設定ファイル内でいい感じに吸収してくれるので、特に意識しなくても大丈夫です。
まとめ
上記の他にも環境や既存のコードベースに応じた修正を行う必要があるかと思いますが、 導入にあたりコアとなった作業は以上です。
地味な作業も多かったですが、コツコツがんばりました。
最終的なdiffはこんな感じです。↓
+の部分はほとんどが yarn.lock ですが、なかなかのボリュームですね。
成果としては、開発時の待ち時間も軽減され、なかなか良かったのではないかと思います。
Webpacker自体に関してですが、実際にはWebpackerが生成するボイラープレートに賛否両論あったりもするようです。個人的には、今まではRails+フロントエンドの環境構築は人それぞれだったものが、Rails Wayとして示されたこと自体が一つのメリットだと考えています。
カスタマイズしようと思った場合には、最終的にはある程度フロントエンド側の知識も必要となりますが、うまく使えれば様々な恩恵を受けられるかもしれません。
採用
MisocaではフロントエンドLOVEなエンジニアを募集中です!