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

webpackを使った Rails上でのReact開発

はじめに

こんにちは、投稿開発部エンジニアの芳賀です。

既存のRailsプロジェクトの中でReact.jsを利用する機会があったので、その時にやったことについてまとめてみます。

私自身は普段RailsのサーバサイドとCoffeeScriptが中心で、最近のJavaScript開発環境についてあまりキャッチアップできていなかったのですが、それらの状況を把握しつつ試行錯誤で開発していった経験から、できるだけ「React採用してみたいけどJavaScript界隈よくわからない目線」で書いてみようと思います。

RailsでReact.jsを使ういくつかの方法

2016年時点で、RailsでReact.jsを使う方法はいくつかあって、どれを採用するかで悩みました。

  1. vendor/assets/javascripts にreact.jsを置いて利用する
  2. react-rails gem を利用する
  3. browserify-rails で npm管理して利用する
  4. railsプロジェクト内に、JavaScript開発用のディレクトリを用意して webpack + babel-loader で利用する

調査したところ、だいたいこんなパターンがあると思っていて、下に行くほどRailsよりもJavaScript開発の知識が必要になってくるイメージでした。

最終的にはwebpackを選択したのですが、それぞれ軽く振れておくと

vendor/assets にライブラリを置く

Railsで外部JSファイルを利用する場合、vendor/assets にダウンロードしたファイルを置いて Sprocketsのマニフェストファイルで読み込んで利用するのが1番手軽だと思います。

手軽だとは思いますが、Reactを使いはじめると他のnpmモジュールもどんどん使いたくなってきて、それらを全部 vendor/assets に入れて sprockets で読み込み順を考えながら開発していくのは、ごく簡単なReactアプリケーションでもすぐ辛くなる印象でした。もっと良い環境を作った方が最終的に楽になると思います。

react-rails

https://github.com/reactjs/react-rails

その名の通り、RailsでReactを利用するためのGemです。 Bundlerで react-rails をインストールするだけで、すぐ利用できるようなお膳立てをしてくれます。

React.jsのファイルが同梱されているのはもちろん、Rubyで設定を書けたり、Reactコンポーネントのレンダリングヘルパーが用意されていたり、coffeescriptでもes2015でもJSXでも書けるなど、JavaScriptよりもRailsやRubyに慣れている場合、かなりとっつきやすいです。

React以外のnpmライブラリは自分でなんとかする必要があります。

browserify-rails

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

React用というわけではありませんが、browserifyというJSビルドツールをRailsのSprocketsで利用できる browserify-rails は、JSモジュールをnpmで管理できて、baberifyを通せば、es2015やJSXの変換もできます。

react-railsのヘルパー関連が必要なければ、browserify-rails のnpmで管理したreactを利用するのも手だと思います。

ただ、開発時のビルドが結構遅くて辛くなってきたのと、既存プロジェクト固有のコードと相性がよくなかったため採用は見合わせました。

browserify-railsに関しては、弊社外村の http://techlife.cookpad.com/entry/2015/12/14/130041 の記事が詳しいです。

webpack

http://webpack.github.io

webpackは依存関係のある分割されたJSやクライアントサイドのアセット群を、いい感じにまとめてくれるビルドツールです。

webpackにはLoaderという仕組みがあり、ソースコードに適用する処理を柔軟に設定できるのですが、babel-loaderを使うことでes2015やJSXで記述したJSファイルを変換することができます。

Reactに関わるモジュールバンドリング(複数ファイルの結合)、ソースコードの変換、ビルドしたコードの配置まではwebpackで行い、ファイルへのフィンガープリント付与などはこれまで通りSprocketsに任せます。

このやり方の場合、Rails開発者がある程度webpack環境について理解する必要があり、若干コストが高いような気もするのですが、今回JavaScriptのコードを触る人間が限られていたのと、JSを触らない開発者はある程度気にしないでもRailsの開発はできるような状態にしておきました。

また、私自身がReact、es2015などをはじめて使ったこともあり、問題があった時に切り分けが簡単であることが重要だったのと、開発中のビルドが速いということが決め手となり採用しました。

webpack利用時の構成

ディレクトリの構成は、以下のような感じで プロジェクトルートの client が webpack環境となっています。

├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── app
├── bin
├── client
├── config
├── config.ru
├── db
├── lib
├── log
├── public
├── test
├── tmp
└── vendor

webpack環境のディレクトリは

client
├── src
├── node_modules
├── package.json
└── webpack.config.js

このような構成になります。

client の 構成は一般的なwebpackによるreact開発とほとんど変わらないのですが、 ビルド時にファイルを配置する場所に ../app/assets/javascripts/webpack を指定しています。

client/node_modules../app/assets/javascripts/webpack はバージョン管理対象外としたいので .gitignore に追加しておきます。

# .gitignore
/client/node_modules
/app/assets/javascripts/webpack

webpack + babelの設定

webpack環境の準備をします。 必要な作業は以下の様な感じです。

  1. package.json を作り、npmでライブラリをインストールする
  2. webpack.config.js でビルド設定を書く
  3. foreman で webpack buildプロセスを起動する

1. package.json を作り、npmでライブラリをインストールする

client ディレクトリで

$ npm init -y

を実行して、package.json を生成します。公開することはないので「private」にしておきます。

{
  "private": true,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

次に webpack と babel関連のライブラリをインストールします。 npm install -D は 開発時にのみ必要なライブラリをインストールしつつ、packpage.jsonに依存関係を記述してくれるオプションです。

$ npm install -D webpack babel-loader babel-preset-es2015 babel-preset-react

「babel-xxxxxx」が多くて混乱するのですが、 babel-loader はwebpackからbabelを使ってトランスパイルするためのパッケージ。 babel-preset-xxxxx は、es2015やJSXを変換するためのプリセットがbabel本体と分離しているので 個別にインストールする必要があります。

なんとなくBabelをインストールすればいい感じに全部やってくれるんでしょ?と思っていたのですがそうではありませんでした。

そして、いよいよ react をインストールします。 -S オプションは、アプリケーションに必要なライブラリを、packpage.jsonに追加しつつインストールをします。

$ npm install -S react react-dom

2. webpack.config.js でビルド設定を書く

次に webpackのビルド設定を書いていきます。 ここでは最小限やりたいことの

  • ソースコードのエントリファイルを指定する
  • 出力先のルールを設定する
  • 出力する際に、Babelによるトランスパイルを設定する

を、記述していきます

// webpack.config.js
module.exports = {
  entry: {
    app: './src/index.js',
  },

  output: {
    path: '../app/assets/javascripts/webpack',
    filename: '[name].js',
  },

  module: {
    loaders: [
      { test: /\.(js|jsx)$/,
        loader: "babel",
        exclude: /node_modules/,
        query: {
          presets: ["es2015", "react"],
        }
      },
    ]
  },
}

これで client/src/index.js がある状態で

$ ./node_modules/.bin/webpack -w

を実行すれば、ファイルの変更を監視して Railsの app/assets/javascripts/webpack/app.js にビルド結果が配置されるようになります。

さらに packpage.jsonに npm scripts に開発ビルドと本番ビルドのコマンドを用意しておくと、foremanやcapistranoから実行するときに便利です。

{
  "private": true,
  "scripts": {
    "webpack-watch": "webpack -w",
    "webpack-build": "webpack -p"
  },
  "devDependencies": {
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.9.0",
    "babel-preset-react": "^6.11.1",
    "webpack": "^1.13.1"
  },
  "dependencies": {
    "react": "^15.2.1",
    "react-dom": "^15.2.1"
  }
}

3. foreman で webpack buildプロセスを起動する

このままでは、Rails開発者もわざわざJSビルド用の別プロセスを起動しておかなければならないので foreman start で railsとwebpackのプロセスを起動するようにします。

# Procfile
rails: bundle exec rails server
webpack: npm --prefix client run webpack-watch

その他

今回は最低限の設定のみふれましたが、webpackの設定で アプリケーションコードと react などのベンダーコードを分けて あまり変更のないベンダーライブラリをキャッシュしやすくしたり

複数のアプリケーションコードに分割しておくことができます。

https://webpack.github.io/docs/code-splitting.html#split-app-and-vendor-code https://webpack.github.io/docs/code-splitting.html#multiple-entry-chunks

最後に

今回は RailsでReactを利用する際に、react-railsやbrowserify-railsを利用しないアプローチについて書いてみました。 誤解のないようにお伝えしておくと、弊社のすべてのプロジェクトでこのアプローチを採用しているわけではなく、 react-railsやbrowserify-railsを使っているプロジェクトもあります。

各々のチームにあった方法を検討する参考になれば幸いです。

クックパッドでは エンジニアを積極採用中です。 https://recruit.cookpad.com/

今回のサンプルをこちらにおいておきます https://github.com/func09/react-on-rails-sample

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