Rails + モダンJS環境(Webpack)で新規アプリ作成


Rails5の正式版でましたね！早速触ろう！でも、フロントはWebpackでブラウザの自動更新といった環境で開発したい！ということでRails5 + フロントはwebpackでビルドするサンプルアプリを作ってみました。

なお、今回のコードは以下においておきましたので、ご参考に。
https://github.com/ufotsuboi/rails-webpack-sample

（追記）
続きを書きました。上記のリポジトリは次のコードも含まれていますのでご注意ください。
[SassのビルドもWebpackでHot Module Replacementしたい](http://qiita.com/UFO/items/ae95f7f53d56ad5240ca)


# 方針

RailsでモダンなJS環境にするにはいくつか方法があって、下記URLがわかりやすく、参考にさせていただきました。

[モダンJavaScript開発環境 on Rails](http://techlife.cookpad.com/entry/2015/12/14/130041)

上記の記事ではざっくり言うと

1. 直接ビルドしたものを `public/` に置く
2. ビルドしたものを `app/assets/javascripts` に置いて、Sprocketsを通す
3. `browserify-rails` を使ってSprocketsで依存を解決する

の3種類上げられていましたが、今回は1に近い方法を取りました。

理由としてはHot Module Replacementが使いたかったことと、Sprocketsがやってくれる本番用にハッシュ付きのファイルの出力がwebpackで比較的簡単にできることが挙げられます。ということで大まかな方針は以下のようにしました。

- RailsからはJS周りは一切触らず、npmの方に寄せる
- WebpackでコンパイルしたJSを `public/dist` に配置
- 本番用はキャッシュ対策にハッシュ値をつけたファイル名にする
- 開発用は `webpack-dev-server` でコンパイルしたものを配信

# 環境

- OSX 10.11.5
- Ruby 2.3.1
- Rails 5.0.0
- Node 6.2.2

# 手順

## rails new

まずは本体となるRailsアプリですね。Sprokectsとjsまわりは必要ないので以下のようにしました。

```bash
$ rails new sample_app -S -J
```

ちなみにいつもは大体

- DBはMySQL
- `.gitignore` はgitignore.ioで生成
- テストフレームワークはrspec
- `bundle install` は `Gemfile` をいじってから行う

なので、

```bash
$ rails new sample_app -S -J -d mysql -T -G -B
```

としてます。

この時点でアプリ自体は立ち上がります。

```bash
$ bundle exec rails server
```

## JSビルド環境の構築

### 必要なライブラリのインストール

まずはpackage.jsonを作ります。 `$ npm init` でもいいですし、適当に。できたら、必要なライブラリをインストールします。

```bash
# babel-presetはお好みで
$ npm install --save-dev webpack webpack-dev-server webpack-manifest-plugin babel-loader babel-preset-es2015 babel-preset-es2016

# Reactを使うので以下もインストール
$ npm install --save react react-dom
$ npm install --save-dev babel-preset-react babel-preset-react-hmre
```

Babelの設定をします。Webpackのコンフィグに書いてもいいですが、私はいつも `.babelrc` に記述しています。 `react-hmre` はReactを使用した場合にHMRを有効にするためのライブラリですので、開発環境のみpresetsに指定されるようにします。

```.babelrc
{
  "presets": ["es2015", "es2016", "react"],
  "env": {
    "development": {
      "presets": ["react-hmre"]
    }
  }
}
```

### Webpack設定: 本番用

方針でも書いたとおり、本番ではキャッシュ対策にハッシュ値付きのファイル名で出力し、`webpack-manifest-plugin` によって `manifest.json` を出力。 `UgilifyJSPlugin` でminifiyも行っています。また、Reactでは本番環境のビルド時に環境変数NODE_ENVに `production` を指定するように推奨されています。（minifyするとwarningが出る）ただし、環境変数を指定するだけではWebpackでのビルド時には参照されません。そこで、 `webpack.DefinePlugin` を使ってビルド時に実際の環境変数の値で置き換えるようにしています。

```webpack.config.prod.js
const path = require('path');
const webpack = require('webpack');
const ManifestPlugin = require('webpack-manifest-plugin');

module.exports = {
  entry: {
    bundle: './src/js/main.js',
  },
  output: {
    path: path.join(__dirname, 'public/dist'),
    filename: '[name]-[hash].js',  // これでハッシュ値付きのファイルが出力される
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({ // ビルド時に環境変数を置き換えてくれるように設定できるプラグイン
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
    new webpack.optimize.UglifyJsPlugin({ // minifiy
      minimize: true,
      compress: {
        warnings: false,
      },
    }),
    new ManifestPlugin(),  // manifest.jsonを出力するプラグイン
  ],
};
```


これで設定はOKなので、簡単に実行できるようにnpm scriptに登録しましょう。 `progress` と `colors` オプションを指定しておくと進捗がわかりカラーで表示されるのでおすすめです。

```package.json
{
  ...
  "scripts": {
    "build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors"
  },
  ...
}
```

ただ、このままだと出力のファイル名にhashが使われてる関係上、内容を変更してbuildする度にファイルが増えます。なので、私は以下のように `rimraf` をもちいて、build前に出力ディレクトリ毎削除するようにしています。

```bash
$ npm install --save-dev rimraf
```

```package.json
{
  ...
  "scripts": {
    "clean": "rimraf public/dist",
    "webpack": "NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors",
    "build": "npm run clean && npm run webpack"
  },
  ...
}
```

これで、 `src/js/main.js` に適当なファイルを用意しておけば、 `public/dist/` に変換後のファイルと、その対応が書かれた `manifest.json` が出力されていると思います。

### Webpack設定: 開発用

もっとも重要な部分、これがあるからWebpackを使っていると言っても過言ではない（言い過ぎ？）

開発時には本番用のとは違いwebpack-dev-serverを利用して、Hot Module Replacementができるようにします。まずは設定ファイルです。

```webpack.config.dev.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    bundle: './src/js/main.js',
  },
  output: {
    path: path.join(__dirname, 'public/dist'),
    filename: '[name].js',  // ハッシュなし
    publicPath: 'http://localhost:8080/', // webpack-dev-serverのURLを指定する
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
  ],
  devServer: { // webpack-dev-serverの設定
    contentBase: 'public/dist',
  },
};
```

```package.json
{
  ...
  "scripts": {
    ...,
    "dev": "NODE_ENV=development webpack-dev-server --config webpack.config.dev.js --progress --colors --inline --hot",
    
  },
  ...
}
```

やることは3つです。

まずは、`publicPath` にwebpack-dev-serverのURLを指定することです。webpack-dev-serverはデフォルトでlocalhostの8080番ポートで起動するので、ここでは `http://localhost:8080/` を指定しています。

次に、`devServer` でwebpack-dev-serverの設定をする。ここではルートとなるファイル出力先ディレクトリを `contentBase` に指定しているだけですが、他にもwebpack-dev-serverの立ち上がる `host` や `port` などの設定をすることが出来ます。

最後に、起動コマンドに `--inline` と `--hot` を付けることです。デフォルトではiflameモードで立ち上がりますが、今回はJSファイルのみなのでinlineモードじゃないと上手く動きません。 `--hot` を付けることでHMRが有効になります。これらは `webpack.config.js` でも設定することができるのですが、どちらか一方にしてください。私はわかりやすいので起動時のオプションを付けることにしました。

webpack-dev-serverについては私もよくわかっていないところが多いのですが、今回試してみるにあたって、下記の記事が詳しく参考にさせていただきました。

[webpack-dev-serverの基本的な使い方とポイント](http://dackdive.hateblo.jp/entry/2016/05/07/183335)

これで、

```bash
$ npm run dev
```

で、`http://localhost:8080` でサーバが立ち上がり、ファイルの変更を監視。変更があればブラウザを自動更新してくれます。

## Rails側のjsファイル読み込み

これまでの設定によって、本番環境なら `manifest.json` に書かれてるファイルを読み込む。開発時にはwebpack-dev-serverの配信するファイルを読み込むとしなければなりません。そこでRails Helperを作って読み込むファイルを決定します。

まずはinitilizerに `manifest.json` を読み込む設定を書きます。initializerのファイル名はなんでもいいですが、ここでは `assets_manifest.rb` としました。

```config/initializers/assets_manifest.rb
Rails.application.config.assets_manifest =
  if File.exist?(Rails.root.join('public', 'dist', 'manifest.json'))
    JSON.parse(File.read(Rails.root.join('public', 'dist', 'manifest.json')))
  end
```

次に、パスを受け取って、環境・ `manifest.json` の内容によって、ただしいパスを返すヘルパーを実装します。

```app/helpers/application_helper.rb
module ApplicationHelper
  def assets_path(path)
    return "http://localhost:8080/#{path}" if Rails.env.development?
    manifest = Rails.application.config.assets_manifest
    path = manifest[path] if manifest && manifest[path].present?
    "/dist/#{path}"
  end
end
```

これで、jsファイルを読み込むときのパスに `assets_path('bundle.js')` を指定すれば、開発環境では `http://localhost:8080/bundle.js` が、本番環境では `manifest.json` に書かれた `/dist/bundle-[hash].js` が読み込まれます。

注意としてはInitializerで `manifest.json` を読み込んでいるため、 `npm run build` 後にRailsアプリを起動する必要があります。

## 起動

以上により、それぞれ以下でアプリを起動することができます。

### 本番環境

```bash
$ npm run build
$ RAILS_ENV=production bundle exec rails server
```

### 開発環境

```bash
# どちらも動かしておく必要あり
$ npm run dev
$ bundle exec rails server
```

# 終わりに

webpack便利！webpack-dev-server便利！

...はい。Rails5である必要何もなかったですねｗ

今回は諸事情でRailsの上にフロントをのせるような形にしましたが、実際のプロダクトではRails5のAPIモードでAPIサーバーとフロントといったような形でわけるのが無難かな？と思います。また、Rails5でES6で書けるようになったのでレールに乗るといった選択もあるでしょう。

とはいえ、JSはどんどん新しいものが出てくるので、それらをすぐに使えるようにnpm側に寄せてしまうのは割と気に入っています。最近はglupなどを使用せず、Webpackだけで済ますことも多いように思いますし、個人的には設定しやすくて良いですね。

今回は出番ありませんでしたが、Rails5で追加された機能も使っていきたいと思います。

# 参考

- [モダンJavaScript開発環境 on Rails](http://techlife.cookpad.com/entry/2015/12/14/130041)
- [webpack-dev-serverの基本的な使い方とポイント](http://dackdive.hateblo.jp/entry/2016/05/07/183335)
- [webpackで作るSprockets無しのフロントエンド開発](http://engineer.crowdworks.jp/entry/2016/05/24/174511)