Webpacker使わずにDockerでRailsとReactを使う

Rails 5.1からwebpacker gemを使う事で、フロント部分にReactやAngularを手軽に使えるようになりました。

しかし、開発速度やリリース頻度、文化の違うシステムをラッピングして他のシステムに同梱するというのは非常に難しく、バージョンアップについて行けないとか、新しい機能が使えないという状況になりがちです。

2017年末現在、フロントエンドのビルドはWebpackerが主流ですが、ちょっと前までGulpやGruntsが主流でした。RailsからWebpackが簡単に呼べるようになりましたが、Webpackに依存しないためDockerを使ってRailsとReact(Node)を使う方法を紹介します。


## Dockerを使ってRails環境を作る

これは色々なページで紹介されているので、詳細は割愛して設定ファイルだけ載せます。

### Dockerfile
```Dockerfile
FROM ruby:2.4.2
ENV LANG C.UTF-8

RUN apt-get update && \
    apt-get install -y nodejs mysql-client --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/
RUN bundle install

COPY . /usr/src/app

EXPOSE 3000
CMD ["./bin/rails", "server", "-b", "0.0.0.0"]
```

### docker-compose.yml
```docker-compose.yml
rails:
  build: .
  command:
    [ "bash", "-c", "rm -f tmp/pids/server.pid; ./bin/rails server -b 0.0.0.0" ]
  environment:
    DISABLE_SPRING: "1"
  ports:
    - "3000:3000"
  volumes:
    - ".:/usr/src/app"
  links:
    - "mysql"

mysql:
  image: mysql:5.6
  environment:
    MYSQL_ROOT_PASSWORD: "passwd"
```

### プロジェクト作成
```shell
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails'" >> Gemfile
touch Gemfile.lock
docker-compose build
docker-compose run rails rails new . --force --database=mysql --skip-bundle
docker-compose build
```

### database設定

config/database.ymlを書き換えます

```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: "passwd"
  host: mysql

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

production:
  <<: *default
  database: <%= ENV['DATABASE_NAME'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
```

### 初期化して起動

```shell
docker-compose run rails rails db:setup
docker-compose run rails server
```

これで http://localhost:3000/ を開くといつもの画面が表示されるはずです。

## ReactのためにNodeの環境を追加する

ReactをビルドするためにNodeの環境を追加します。

今回はWebpackやBrowserifyの代わりに[Parcel bundler](https://github.com/parcel-bundler/parcel)を使ってみたいと思います。「[webpack時代の終わりとparcel時代のはじまり - Qiita](https://qiita.com/bitrinjani/items/b08876e0a2618745f54a)」に触発されました。


まずはRAILS_ROOT直下にreactディレクトリを作成し下記の様なファイルを設置します。

### react/Dockerfile
```react/Dockerfile
FROM node:8

WORKDIR /usr/src/app

ADD yarn.lock /usr/src/app/yarn.lock
ADD package.json /usr/src/app/package.json
RUN yarn install

COPY . /usr/src/app
```

### react/.babelrc
```react/.babelrc
{
  "presets": ["env", "react"]
}
```

### docker-compose.ymlに下記を追加
```docker-compose.yml
react:
  build: react
  volumes:
    - "/usr/src/app/node_modules"
    - "./react:/usr/src/app"
    - "./app/assets/javascripts/react:/usr/src/app/dist"
  command:
    [ "./node_modules/.bin/parcel", "watch", "src/index.js" ]
```

### コマンドで空ファイルなどを作成
```shell
mkdir react/dist react/src
touch react/yarn.lock
echo '{"name": "app","version": "1.0.0"}' > react/package.json
echo 'console.log("Hello React")' > react/src/index.js
```

### Reactのモジュールをインストール
```shell
docker-compose build
docker-compose run react /bin/bash -c "yarn add react react-dom && yarn add babel-preset-react babel-preset-env parcel-bundler --dev"
docker-compose build
```

これで、`react/src/index.js`を監視して変更があればビルドして`app/javascripts/react/index.js`が生成されます。


## RailsからReactを呼び出す

上に書いたように`app/javascripts/react/index.js`にReactを含んだJavascriptが生成されるので、`app/javascripts/application.js`から`=require`で読み込みます。

これで全てのページでReactが読み込まれています。次はReact側のコードから読み込まれているページを知る為にmetaタグをlayoutに組み込みます。
同時に、`meta_tags`gemを`Gemfile`に追加してください。

```app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
  <title>Sample page</title>
  <%= display_meta_tags(
    controller_name: controller_name,
    action_name: action_name,
    params: "{}"
  ) %>
  <%= stylesheet_link_tag "application", media: "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
```

これで全てのページでcontrollerとaction名が取得できます。

次に。これを元に読み込むJavascriptを振り分けるコードを一番最初に読み込まれるJavascriptに書きます。

```react/src/index.js
function metaValue(name) {
  return document.querySelector(`meta[name="${name}"]`).getAttribute("content");
}
var action = `${metaValue('controller_name')}/${metaValue('action_name')}`;

if("welcomes/show" == action) {
  require('./welcomes/show/index.js')
}
// else if("welcomes/index" == action) {
//   require('./welcomes/index/index.js')
// }
```

### erbからReactに値を渡す

例えば、@foo_, @bar_を渡す場合には下記の様にmeta_tagにjsonを入れて渡して、これをJavascriptで復号してください。

```index.html.erb
<%= set_meta_tags(
  params: {
    foo: @foo_,
    bar: @bar_
  }.to_json
) %>
<div id="container">Loading</div>
```