Webpacker is installed 🎉 🍰

こんにちは、@mugi_uno です。

庭に花壇を作ったところ変に日焼けをしてしまい、半袖で外に出るのが恥ずかしいです。

つい最近、Misocaのフロントエンド周りのビルドをWebpackerを利用したものに置き換えました。 様々な知見が得られたので書いてみたいと思います。

Webpacker?

フロントエンド用のビルドツールであるwebpackRailsから簡単に使えるようにするためのRubyGemsです。

github.com

f:id:mugi1:20170619094144p:plain

Rails5.1からはこのWebpacker経由で、rails new の際に同時にwebpack用の設定ファイルを生成することもできます。

流行りのフロントエンド環境を構築しようとする際、最近ではwebpackを利用するケースが多いですが、実際にはES6やReactなどが混ざることも多く、慣れていないと設定するだけでもなかなか敷居が高いです。

さらに、Railsと組み合わせる場合には、アセットパイプラインとの兼ね合いをどうするか?などといった課題も登場するため、考えるだけで鼻血が出そうになります。

Webpackerを利用することで、その辺りをいい感じに解消してくれるというわけです。

Misocaでの導入理由

browserify-rails めっちゃ時間かかる問題

もともとはアセットのビルドに browserify-rails を利用していましたが、ビルド時間がMisoca内で問題になっていました。キャッシュによる高速化が効いてはいるものの、何かの拍子にフルビルドが走ると数分待たされることも。

CIでも上記の影響を受けるため、開発効率にあまりよろしくない影響を与えていました。

(開発効率が落ちた図) f:id:mugi1:20170609181108p:plain f:id:mugi1:20170609181116p:plain f:id:mugi1:20170609181127p:plain

なんとかしたい

最初はbrowserify-rails自体のチューニングを検討しましたが、webpackでのビルドを一度試してみたところ、そもそもフルビルドでも20秒くらいで終わることがわかりました。

MisocaではRails5.1を導入済みだったのもあり、Webpackerを使って本格的に導入を試みることに。

webpackerがやってくれること

※以下内容は rails@5.1.1/webpacker@2.0 を前提としています。

大きく分けると以下の3つかと思います。

  1. rakeタスクの提供
  2. ボイラープレートの生成
  3. 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 🎉 🍰 」は、このタスク成功時の出力結果です。

f:id:mugi1:20170616133609p:plain

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 などは単体で提供されています。

github.com

グローバル参照を解決

値がグローバル定義されているのを前提としているコードがある場合、グローバルに値を明示的に定義しないとアクセス不可となることがあります。(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 ですが、なかなかのボリュームですね。

f:id:mugi1:20170616132756p:plain

成果としては、開発時の待ち時間も軽減され、なかなか良かったのではないかと思います。

Webpacker自体に関してですが、実際にはWebpackerが生成するボイラープレートに賛否両論あったりもするようです。個人的には、今まではRails+フロントエンドの環境構築は人それぞれだったものが、Rails Wayとして示されたこと自体が一つのメリットだと考えています。

カスタマイズしようと思った場合には、最終的にはある程度フロントエンド側の知識も必要となりますが、うまく使えれば様々な恩恵を受けられるかもしれません。

採用

MisocaではフロントエンドLOVEなエンジニアを募集中です!