React-Redux on Rails(w/webpack/yarn)環境を作ったメモ


## やりたいこと

はいからなフロントエンド開発にあこがれてReact(とReduxその他)を最近はじめました。

とりあえず`Babel/browser.js`を使って画面をロードするたびに毎回トランスパイルするような環境で勉強していたのですが、実務レベルで利用していくにあたって以下のような環境を作ることが課題です。

1. BabelによってReactコード(JSX/ES6)を静的にトランスパイルし、ファイル出力する
2. ファイル(モジュール)単位に分散させたコードを依存解決し、バンドルしてファイル出力する
3. 1-2を単体テストや難読化等と組み合わせてタスク化し運用する
4. 1-3をサーバサイドのタスクやデプロイ手順に組み込み、同時に運用する


Rails上にこのような環境を構築するためのツールとしては以下がみつかりまして、

ツール  | 機能
---     | ---
[gem sprockets-commoner](https://github.com/Shopify/sprockets-commoner) | アセット作成と同時にAltJSをトランスパイルして、アセットに組み込めるようにする
[gem webpacker](https://github.com/rails/webpacker)                     | Rails公式。webpack/yarnベースでフロント環境構築を補助。Rails5.1で正式対応。Reactなど復数フレームワーク対応
[gem react-rails](https://github.com/reactjs/react-rails)               | React公式。webpack/npmベースでフロント開発構築を補助。ReactオブジェクトをRailsのView側から操作できる機能なども


試しにwebpackerをちょっと使ってみたところ非常に簡単に環境構築できました。ただ裏側で色々やってくれすぎてしまって、設定ファイルなどなかなか複雑な様子。

ツール類の理解が不十分な状態でこれを運用するのには不安が大きく、やはり自力で最小構成を作って順次改善していこうという流れになりました。

以下、上に挙げた課題1と2を満たす環境の構築手順についての覚え書きです。


## 概要

### 利用するパッケージ

今回は主に以下のパッケージを利用して、フロント環境をRails上に構築する。


npmパッケージ   | 用途
---             | ---
yarn            | フロントのパッケージ管理(npmの代替)
webpack         | フロントのバンドル(ファイル結合・依存関係解決)
babel           | Reactコード(ES6, JSX)のトランスパイル
react           | コンポーネントの構築・管理
redux           | 状態モデルの構築・管理
immutable       | Redux reducerの実装時に不変オブジェクトを利用
bootstrap       | UIデザイン用
jquery          | UIデザイン用


- ruby, rails, node, yarnをローカル(のグローバル)にインストール済みの前提
- 管理用のツールの使い方については別途調査した:
    - [webpackチートシート](http://qiita.com/morrr/items/0f35adc38b59f5a67141)
    - [yarnチートシート](http://qiita.com/morrr/items/558bf64cd619ebdacd3d)


### ファイル構成

フロント環境に関連するファイルは以下のように配置する。

```
app/
    assets/
        javascript/     // アセットパイプライン管理のスクリプトを格納(今回は関係なし)
    scripts/            // webpackのバンドル対象のjsファイルを格納
        actions/            // Reduxのアクション
        components/         // Reactプレゼンテーションコンポーネント
        containers/         // Reactコンテナコンポーネント
        reducers/           // Reduxのリデューサ
        entries/            // webpackのエントリポイント
public/
    scripts/            // webpackがバンドルしたjsファイルを(自動的に)格納

node_modules/           // npmパッケージを格納
package.json            // yarnによるパッケージ情報
yarn.lock               // yarnによるバージョン設定
webpack.config.js       // webpackの設定を記述
```

## 構築

### プロジェクトの作成

作成済みの場合は省略

```bash
$ rails new rrrdemo
$ cd rrrdemo
$ bundle install
```

```:.gitignore
# バンドルファイルを管理から除外
public/scripts/*
```

### npmパッケージのインストール

必要なパッケージをインストールする。

```bash
# 管理状態を初期化
$ yarn

# パッケージをインストール
$ yarn add react react-dom react-redux redux immutable jquery bootstrap
$ yarn add --dev webpack webpack-dev-server babel-loader babel-core babel-preset-es2015 babel-preset-react
```

### webpackの設定

webpackの設定ファイルを作る。

```js:webpack.config.js
var path = require('path');

module.exports = {
    // エントリの定義
    entry : {
        // TODO: SPA(画面)が増えるたびに逐次追加する
    },

    // 出力先の定義: /public/scripts/<エントリ名>.bundle.js に出力される
    output : {
        path        : path.resolve(__dirname, 'public/scripts/'),
        filename    : '[name].bundle.js',
    },

    // ローダーの設定: .jsファイルはバンドル前にbabelでトランスパイルする
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: [ 'es2015', 'react' ],
                    },
                }],
            },
        ],
    },

    // webpack-dev-serverの設定
    devServer : {
        port            : 8080,
        inline          : true,
        clientLogLevel  : 'info',
        contentBase     : path.join(__dirname, 'scripts/'),
        watchOptions    : {
            poll            : true
        },
    },

    // Source Mapの設定
    devtool : "cheap-eval-source-map",
};
```

### node_modulesの設定

`node_modules/`をバージョン管理に含める必要はないのでignoreしておく。(Rails5以降デフォルト)

```:.gitignore
/node_modules
/yarn-error.log
```

`node_modules/`以下のファイルをアセットパイプラインからも参照できるようにしておく。アセットを使わない前提の場合は不要(今回はbootstrap.cssをアセット経由で使いたい)
(これもRails5以降デフォルト)

```ruby:config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join('node_modules')
```


## 確認

デモ用の画面を2つ作り、それぞれが依存解決されたバンドルファイルを実行できることを確認する。

### デモ画面の作成

各画面は、webpackが作成するバンドル済みのファイルを読めるようにしておく。

```bash
$ rails g controller demo
```

```ruby:config/route.rb
+ get '/demo/a' => 'demo#a'
+ get '/demo/b' => 'demo#b'
```

```ruby:app/controllers/demo_controller.rb
+ def a
+ render 'a'
+ end
+ 
+ def b
+ render 'b'
+ end
```

```css:app/assets/stylesheets/application.css
+ @import 'bootstrap/dist/css/bootstrap';
```

```app/views/demo/demo_a.html.erb
<h3>Demo A</h3>
<div class="container">
  <div class="row">
    <div class="col-md-12">
      <div id="sandbox"></div>
    </div>
  </div>
</div>
<script src="/scripts/demo_a.bundle.js" type="text/javascript"></script>
```

```app/views/demo/demo_b.html.erb
<h3>Demo B</h3>
<div class="container">
  <div class="row">
    <div class="col-md-12">
      <div id="sandbox"></div>
    </div>
  </div>
</div>
<script src="/scripts/demo_b.bundle.js" type="text/javascript"></script>
```


### エントリポイントの登録

各画面のエントリファイルを(まずはファイルだけ)作成する。

```js:app/scripts/entries/demo_a.js
console.log('demo A loaded');
```

```js:app/scripts/entries/demo_b.js
console.log('demo B loaded');
```

エントリファイルをwebpackの設定に追加する。

```diff:webpack.config.js
    entry : {
+        demo_a  : path.resolve(__dirname, 'app/scripts/entries/demo_a.js'),
+        demo_b  : path.resolve(__dirname, 'app/scripts/entries/demo_b.js'),
    },
```

### 開発環境の起動

デモ用のサーバを起動する。
今回はwebrick + webpack(watchモード)を使う。
(webpack-dev-serverも使えるが、erbを解釈しないので後々面倒になりそう)


```bash
# それぞれ別窓やバックグラウンド等で同時に実行する
$ rails s
$ webpack --watch --progress --config webpack.config.js
```

以下のURLを開き、画面表示とコンソールのログ表示を確認する。

- `http://localhost:3000/demo/a`
- `http://localhost:3000/demo/b`

以降はjsファイルを保存して画面リロードするだけ最新状態が確認可能になる。


### デモの実装

jQueryとReactが使えることを確認してみる。


デモ用のスクリプトを作る。

```js:app/scripts/components/Message.js
import React from 'react';
import ReactDOM from 'react-dom';

export default class Message extends React.Component{
    render(){
        return (<div className="alert alert-success" role="alert">
            {this.props.label}
        </div>);
    }
};
```

```js:app/scripts/entries/demo_a.js
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import Message from '../components/Message';

$(function(){
    ReactDOM.render(
        <Message label="Demo A" />,
        $('#sandbox')[0]
    );
});
```

```js:app/scripts/entriesdemo_b.js
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
import Message from '../components/Message';

$(function(){
    ReactDOM.render(
        <Message label="Demo B" />,
        $('#sandbox')[0]
    );
});
```

デモ用の画面を確認する。

- `http://localhost:3000/demo/a`
- `http://localhost:3000/demo/b`

各画面にReactコンポーネント(緑色のアラート表示)が表示されることが確認できたらOK。

![demoA.png](https://qiita-image-store.s3.amazonaws.com/0/66479/946f6dc8-5475-84a4-562a-a39d0e98881e.png)

![demoB.png](https://qiita-image-store.s3.amazonaws.com/0/66479/1a443b24-e2b5-8254-ed82-4768a445fd37.png)