こんにちは、WantedlyのWebエンジニアの高松です。
このエンジニアブログでも何度かReact関連の話題が出ていますが、WantedlyではReact + Reduxを中心としたWebフロントエンドの技術スタックを導入して開発しています。
今回はスタックの導入方法や勘所について、詳しく解説してみたいと思います。特に既存のRails環境にReactなどの導入を検討していらっしゃる方の参考になれば、と思います。
今回の内容は以前発表した以下のスライドを元にしています。
これまでのWantedlyのフロントエンド開発
まず、今回のスタック導入前のWantedlyのフロントエンド開発がどのようなものであったのかについてご説明します。
Wantedlyの開発リポジトリを参照すると、最初のコミットは2011年9月になっています。現在は社内でもマイクロサービス化の動きが始まっていますが、基本的にWantedlyはモノリシックなRailsアプリケーションとして開発されています。5年近く活発に開発が続けられてきた結果、現在は350近いモデルを持つ規模のRailsアプリケーションとなりました。
WantedlyはRails Wayに則った開発を基本としており、JavaScriptに関しても同様でした。Asset Pipeline・Sprocketsの仕組みに乗り、AltJSとしてはcoffee scriptが使われていました。多くはjQueryで書かれていますが、Backbone.js・Angular.jsが導入されている部分もあります。
しかし、直近で導入されたAngular.jsは、メンテナンスできる人もおらず、負債化していると言っても過言ではない状態でした。開発できる人、それをレビューできる人、さらにバージョンアップなども含めてアップデートする人が(ほぼ)いなくなってしまったからです。これはAngular.js自体の良し悪しの問題ではなく、社内での広め方など運用面での問題だと感じています。
そういった経緯を経た結果、最近のWantedlyのフロントエンド開発はjQueryを使うのがメインストリームな状況でした。クラスの責務分割を意識した設計や、Event Emitterの導入によるPub/Subなど良い実装を目指してきましたが、Single Page Applicationのような複雑な構成や、実装量が1万行を超えるような大規模な開発には対応しきれないことが予想されます。そこでフロントエンド開発に関して改善意識を持ったエンジニアを中心に、新しい技術スタックを導入する動きが始まりました。
導入の方針とスタック
今回の導入は、今までのJavaScriptの資産を全て置き換えるのではなく、新規開発する部分から順次適用する方針です。古いソースコードに関しては、リニューアルや大きな機能拡張が発生したタイミングで、新しいスタックで書きなおしをすることになると思います。
導入したスタックの中で主なものを以下に挙げます。それぞれの詳細な説明はここでは割愛しますが、2016年現在の比較的主流なものを集めた構成になっていると思います。
ES2015 (Babel)
React
Redux
Immutable.js
CSS Modules
webpack
導入時の勘所
既存のRailsアプリケーションに対して新しいフロントエンドのスタックを導入する場合、最大の検討ポイントは"Asset Pipelineと共存するのか、捨てるのか?" という点です。
Asset Pipelineは、coffee scriptやSassのコンパイル、JSやCSSのminify、ハッシュ値(digest)の付与によるキャッシュの最適化などを提供する仕組みです。デプロイ時にも"assets:precompile"するだけで簡単にコンパイル・配置が可能なため、基本的なアセット類の管理や最適化をRailsが担当してくれることになります。
しかし、Wantedlyでは最終的に"Asset Pipelineを捨てる"という決断をしました。当初はコンパイルしたアセットを"assets"配下、つまりAsset Pipelineの適用範囲に出力する方式を考えましたが、中間ファイルを出力するような実装になり、ゼロベースで考えるならば理想的な構成とは言い難いと感じました。Asset Pipelineで提供される仕組みはwebpackのプラグインやOSSのツールによっても可能です。せっかく新しい仕組みを導入するのならば、理想的な構成を目指したい、という思いが決断の背景にありました。
これらの判断にあたっては、joe_reさんの以下の記事に多大な影響を受けています。
ディレクトリ構成
まずRailsプロジェクトのルート・ディレクトリ配下で、appと同階層にfrontendディレクトリを切っています。frontend配下は以下の様な構成になっています。
React + Reduxの構成で使うのは、"watedly/frontend/assets/javascripts/react" 配下です。react配下のディレクトリ構成はReduxのレイヤ分割にあわせた構成になっています。
% tree -d -L 4 wantedly/frontend [11:48:46]
wantedly/frontend
└── assets
├── images
├── javascripts
│ ├── common
│ ├── entries
│ ├── lib
│ │ ├── immutable
│ │ ├── introjs
│ │ ├── polyfills
│ │ └── utils
│ ├── react
│ │ ├── actions
│ │ ├── components
│ │ ├── containers
│ │ ├── reducers
│ │ └── store
│ ├── shims
│ ├── team
│ │ ├── common
│ │ ├── directives
│ │ ├── filters
│ │ └── modules
│ └── vendor
├── stylesheets
│ ├── lib
│ └── mixins
└── templates
└── team
├── project
├── update
└── update_comment
ディレクトリ構成で特徴的なのは、どちらもReactコンポーネントを配置する層ですが、componentsとcontainersのディレクトリを分けていることです。昨年話題になっていた以下のブログ記事に影響をうけ、受け取った値を元に描画し・クリックなどのイベントのハンドリングする"dumb components"にあたるものをcomponentsディレクトリ、各Componentを束ね、Actionの発行をコールする"smart components"にあたるものはcontainersディレクトリに配置しています。ビジネスロジックの分散を避け、コードの見通しを良くすることが狙いです。
ビルドの仕組み
Wantedlyではアセットのビルドにwebpackを利用しています。このレイヤの候補として、Browserifyかwebpackが有力な候補としてあがると思いますが、"マルチエントリーポイントへの対応が可能" "タスクランナーなしでもwebpack単体だけで設定しやすい"ことが主な理由となり、webpackを採用しました。
フロントエンドにビルドの仕組みを組み込む場合、ソースコードの管理の方法は2通り考えられます。
1: ビルド済みのアセットを含めてコミットして管理する
2: ソースのみを管理し、ビルド時に動的にコンパイル済みのアセットを生成する
1の方がデプロイの仕組みを考える必要もなく導入は簡単ですが、開発者のコミット漏れによって想定通り動かないなど、ヒューマンエラーによるミスが起こるリスクがあります。
Wantedlyでは2の方法を採用し、デプロイの仕組みの中にwebpackによるビルドのプロセスが組み込まれています。
開発環境
開発環境ではwebpack dev-serverを利用しています。対象のファイルの更新を検知して、対象範囲が再コンパイルされ、コンパイルされたJS・CSSとimageなどのアセットは開発者のローカルにたてられたwebpack dev-serverから配信されます。
webpack dev-serverを利用するメリットとしては、各種プラグインを利用可能な事が挙げられます。代表的なものとして"Hot Module Replacement" があります。Hot Module Replacementは変更があったコンポーネントのみを、ブラウザ上でAuto Reloadしてくれる仕組みです。特にスタイルの調整など細かな変更を入れる場合には、Hot Module Replacementは非常に有効です。
デプロイ
開発環境以外ではデプロイされたアセットは、"public" 配下に出力されます。Asset Pipelineの管理から外れているため、アセットのminifyやダイジェストの追加は自身で行う必要があります。
WantedlyではUglifyJsPluginでminify、ダイジェストの生成をwebpack-manifest-pluginで行っています。
開発環境とその他の環境でアセットの配置先が変わるため、Rails側でロードする時に環境に応じた切り替えが必要になります。以下のようなヘルパメソッドを作成し、環境毎の違いを吸収しています。
def resolve_webpack_asset_path(path)
if ENV["USE_WEBPACK_DEV_SERVER"].to_b
return "http://localhost:8080/assets/build/#{path}"
end
host = Rails.application.config.action_controller.asset_host
host = host.call if host.is_a?(Proc)
manifest = Rails.application.config.assets.webpack_manifest
if manifest.present? && manifest[path].present?
path = manifest[path]
end
"#{host}/assets/build/#{path}"
end
まとめ
ここまでWantedlyで既存のRails環境にReact + Reduxを中心としたWebフロントエンドの技術スタックを導入した方法について紹介してきました。重要な部分についてまとめます。
- Asset Pipelineと共存するのか? 捨てるのか? が大きな選択となる
- Railsプロジェクト配下のappと同階層にfrontendディレクトリを切っている
- ビルドにはwebpackを利用し、各種プラグインでminifyやdigest生成を行っている
- 開発環境ではwebpack dev-server、その他の環境ではデプロイプロセスの中にwebpackによるビルドを組み込んでいる
- 環境の違いを吸収するためにViewからのアセット読み込み時にはヘルパを用意している
フロントエンドの環境構築を検討中の方の参考になれば幸いです。