WebPackを使ってRailsからJavaScriptを楽に良い感じに分離する

# はじめに
フロントエンドへスプラトゥーンを決めるために、Rails プロジェクト内で JavaScript を楽に良い感じに分離する方法を考えてみました。

先人たちが様々な方法を提示してくださっていましたが、設定が大掛かりだったり、何種類かのツールを活用したりと、自分にはどうもハードルが高かったため、より簡単な方法を模索しました。

# TL;DR
Initial commit で、この記事でこれから行う設定をコミットしています。

- [necojackarc-sandbox/rails_with_webpack](https://github.com/necojackarc-sandbox/rails_with_webpack)

# 特徴
- Sprockets の良い所はそのまま活用
- 導入ツールは実質 WebPack のみ
- 設定項目が少なく簡単
- おそらくほぼメンテナンスフリー

# 概要
以下のような流れで、JavaScript のコーディング、ビルド、配信を行います。

1. `frontend`ディレクトリ以下で JavaScript のコーディングを行う
2. `frontend`以下で作成した JavaScript を`app/assets/javascripts`以下へビルドする
3. ビルド後にできた`app/assets/javascripts`内のファイルを結合せずにそのまま配信する

Sprockets の良い所というのは、Fingerprint を付けて配信してくれる点、つまり`3`の部分だと個人的には思います。
この機能を他のフロントエンド系のビルドツールでカバーしようとすると、大工事やツールが必要になります。

つまり、ここを大人しく Sprockets に任すことで、大掛かりな設定を行うことなく、Rails から JavaScript を分離させることができるようになります。

Views が Rails に残ってしまっていますが、全てのフロントエンドを消し去るのであれば、そもそも Rails API にすれば良いという噂もあるので、これくらいの分離具合が丁度良いんじゃないかなと思います。

## 具体例
概要に少しだけ具体性をもたせると、以下のような感じになります。

1. `frontend/src/javascripts/hoge.js`でモダンな JS 開発を行う
2. WebPack で`frontend/src/javascripts/hoge.js`をビルドし、`assets/javascripts/hoge.js`として出力する
3. Sprockets で`assets/javascripts/hoge.js`に Fingerprint を付けて配信する

## 使用ツールとバージョン
基本的には WebPack のみが追加ツールとなるイメージです。
WebPack でのビルド時に Transpile を行いたかったので、Babel も導入しています。

- Sprockets 3.4.1 (+ Rails 4.2.5)
    - 配信 (with MD5 fingerprint)
- WebPack 1.12.9 (+ Babel 6.1.18)
    - ビルド

## ディレクトリ構成
Rails プロジェクトのルートに`frontend`というディレクトリを追加しています。

```bash:rails_root
$ ls
Gemfile       README.md     app/          config/       db/           lib/          spec/
Gemfile.lock  Rakefile      bin/          config.ru     frontend/     public/       vendor/
```

`frontend`のディレクトリ構成は以下のとおりです。

```bash:rails_root
$ tree frontend -I node_modules
.
├── config
│   ├── development
│   │   └── webpack.config.js
│   └── production
│       └── webpack.config.js
├── package.json
├── src
│   └── javascripts
│       └── application.js
└── test
    └── javascripts
```

おおまかに、

- package.json
- WebPack 設定ファイル
- ソース置き場
- テスト置き場

の4つから構成されています。

# 設定手順
具体的な設定手順について述べていきます。

1. Sprockets で分割配信するように設定する
2. WebPack でのビルドに関する設定を行う
3. WebPack のビルドを Precompile にフックさせる

## 1. Sprockets で分割配信するように設定する
Rails のデフォルトの設定だと、`application.js`に全ての JavaScript ファイルが結合されて配信されるようになっているので、この設定を変更します。

### `app/assets/javascripts`以下を一掃する
`app/assets/javascripts`ディレクトリには、ビルド後の js ファイルが格納される場所となるため、このディレクトリは空にします。

Git 管理下からも外したいので、`.gitignore`に以下のように追記しておきます。

```rails_root/.gitignore
!/app/assets/javascripts/.keep
/app/assets/javascripts
```

また、ついでに CSS も分割配信にしたかったので、`app/assets/stylesheets/application.scss`内の`*= require_tree .`という行も削除しておきました。

### Sprockets の Precompile 対象を追加する
デフォルトでは`application.js`と`application.css`しか対象になっていないため、`config/initializers/assets.rb`に設定を追加し、全ての JavaScript と CSS ファイルを対象にするようにします。

```ruby:config/initializers/assets.rb
Rails.application.config.assets.precompile += %w(*.js *.css)
```

#### アンダースコアから始まるファイルを除去する
Rails で Views 系の Gem を使っている場合、使用している Gem によっては Precompile でエラーが発生する場合があります。

- [rails bootstrap-sass assets compilation error - undefined variable alert-padding](http://stackoverflow.com/questions/21911620/rails-bootstrap-sass-assets-compilation-error-undefined-variable-alert-padding)

その場合は、上記の設定の代わりに、以下のアンダースコアから始まるファイルを対象外とする設定を使用すれば良さそうです。

```ruby:config/initializers/assets.rb
Rails.application.config.assets.precompile << /(^[^_\/]|\/[^_])[^\/]*(\.js|\.css)$/
```

### 補足: Rails での JavaScript と CSS の読み込み方
それぞれ Views テンプレート内で、

- `javascript_include_tag`ヘルパー
- `stylesheet_link_tag`ヘルパー

を呼び出すことで使用可能になります。

## 2. WebPack でのビルドに関する設定を行う
WebPack と、WebPack で Transpiler として使用する Babel をインストールします。
もし`frontend`直下に`package.json`がない場合は、先に`npm init`を実行しておきます。

```bash:rails_root/frontend
$ npm init
$ npm install -D webpack babel babel-loader babel-core
```

### 例: React.js と ES2015 と Stage2 を使う
例として、React.js (0.14.3) と ES2015 と Stage2 を使えるように設定を進めていきます。

まずは、React.js をインストールします。

```bash:rails_root/frontend
$ npm install --save react react-dom
```

次に、必要な Babel の Preset をインストールします。

```bash:rails_root/frontend
$ npm install -D babel-preset-react babel-preset-es2015 babel-preset-stage-2
```

WebPack で Babel を使った Transpile ができるよう、設定ファイルを作成します。

```config/development/webpack.config.js
module.exports = {
  devtool: 'inline-source-map',
  entry: {
    application: './src/javascripts/application.js',
  },
  output: {
    path: '../app/assets/javascripts',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel?presets[]=react,presets[]=es2015,presets[]=stage-2'
      }
    ]
  }
}
```

本番用は devtool オブションを外せばよいと思います。たぶん。

```config/production/webpack.config.js
module.exports = {
  entry: {
    application: './src/javascripts/application.js',
  },
  output: {
    path: '../app/assets/javascripts',
    filename: '[name].js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel?presets[]=react,presets[]=es2015,presets[]=stage-2'
      }
    ]
  }
}
```

これで、`webpack --config config/{enviroment}/webpack.config.js`を叩けば、entry に登録された JavaScript が`app/assets/javascripts/*.js`にビルドされます。

上記の設定ですと、`frontend/src/javascripts/application.js`をエントリーとして、`app/assets/javascripts/application.js`にビルドされます。

作成したい JavaScript ファイルが増えても、entry に新たなエントリーを追加し、それを使いたい Views でヘルパーで読み込むだけで済みます。


### `npm run`で起動できるようにする
`npm run build`のようなコマンドを準備しておくととても快適になるので、

- release: `webpack --config config/production/webpack.config.js"`
- build: `webpack --config config/development/webpack.config.js"`
- watch: `webpack --watch --config config/development/webpack.config.js"`

の3つを package.jsonに登録しておきます。

```rails_root/frontend/package.json
{
  "scripts": {
    "release": "webpack --config config/production/webpack.config.js",
    "build": "webpack --config config/development/webpack.config.js",
    "watch": "webpack --watch --config config/development/webpack.config.js"
  }
}
```

これで`frontend`直下で`npm run build`と叩けば、WebPack によるビルドが実行されます。

## 3. WebPack のビルドを Precompile にフックさせる
最後に本番リリース時の処理に特別な手法を加えなくて済むように、Precompile の直前に、`npm run release`を実行させるようにします。

```rails_root/lib/tasks/before_precompile.rake
task :build_frontend do
  cd "frontend" do
    sh "npm install"
    sh "npm run release"
  end
end

Rake::Task["assets:precompile"].enhance(%i(build_frontend))
```

これで、`rake assets:precompile`を叩くと、その直前に`frontend`ディレクトリ直下で、`npm run release`が実行されるようになります。

# まとめ
Rails から JavaScript を良い感じに分離させるために、

1. WebPack で JavaScripts を`app/assets/javascripts`以下へビルドする
2. Sprockets で`app/assets/javascripts`以下のファイルをそのまま配信する

というアプローチをとってみました。

このアプローチの利点は、設定項目や追加ツールが少なく、メンテも楽だという点だと思います。
無理に Sprockets を置き換えていないことで、JavaScript のビルド設定が非常に単純化されています。

[Why we should stop using Grunt & Gulp](http://blog.keithcirkel.co.uk/why-we-should-stop-using-grunt/) を読んでから、Gulp 無しで簡潔に設定できないかなと考えていたのですが、Sprockets を適度に受け入れてやることで、思いの外楽に実現できました。

良い塩梅の独立具合になってるんじゃないかと思います。

# 追記: 完全分離
Sprockets のメリットは Fingerprint 付きでの配信と書きましたが、それをやってくれる WebPack のプラグインがありました。

- [sporto/assets-webpack-plugin](https://github.com/sporto/assets-webpack-plugin)

こちらを使うと Fingerprint 付きでの配信までが WebPack で完結するため、Rails から　JavaScript を完全分離できます。

どのようにパス解決を行っているかというと、ビルドすると以下のような対応が定義された JSON が出力されます。こちらを用いてパス解決を行います。

```json
{
    "one": {
        "js": "/js/one_2bb80372ebe8047a68d4.bundle.js"
    },
    "two": {
        "js": "/js/two_2bb80372ebe8047a68d4.bundle.js"
    }
}
```

Rails での使用例が [README の Using this with Rails](https://github.com/sporto/assets-webpack-plugin#using-this-with-rails) に載っていますが、非常に簡単そうです。

# 参考文献
- [Why we should stop using Grunt & Gulp](http://blog.keithcirkel.co.uk/why-we-should-stop-using-grunt/)
- [Replacing the Rails asset pipeline with Gulp](https://bugsnag.com/blog/replacing-the-rails-asset-pipeline-with-gulp)
- [Gulp - a modern approach to asset pipeline for Rails developers](http://blog.arkency.com/2015/03/gulp-modern-approach-to-asset-pipeline-for-rails-developers/)
- [Gulp on Rails: Replacing the Asset Pipeline](https://viget.com/extend/gulp-rails-asset-pipeline)
- [フロントエンドエンジニアのための「俺の最近のRailsのJS開発環境」を考察する](http://qiita.com/izumin5210/items/c642ae75f916d301456a)
- [react + rails 構成パターンまとめ。](http://qiita.com/takayukishmz@github/items/d0e86184fc35ab1a825c)
- [RailsのAsset PipelineとPrecompileをNode.jsのみで処理できるgulp-sprocketsを作った](http://waka.github.io/2015/11/25/gulp_sprockets.html)