Rails 5.1からwebpacker gemを使う事で、フロント部分にReactやAngularを手軽に使えるようになりました。
しかし、開発速度やリリース頻度、文化の違うシステムをラッピングして他のシステムに同梱するというのは非常に難しく、バージョンアップについて行けないとか、新しい機能が使えないという状況になりがちです。
2017年末現在、フロントエンドのビルドはWebpackerが主流ですが、ちょっと前までGulpやGruntsが主流でした。RailsからWebpackが簡単に呼べるようになりましたが、Webpackに依存しないためDockerを使ってRailsとReact(Node)を使う方法を紹介します。
Dockerを使ってRails環境を作る
これは色々なページで紹介されているので、詳細は割愛して設定ファイルだけ載せます。
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
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"
プロジェクト作成
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を書き換えます
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'] %>
初期化して起動
docker-compose run rails rails db:setup
docker-compose run rails server
これで http://localhost:3000/ を開くといつもの画面が表示されるはずです。
ReactのためにNodeの環境を追加する
ReactをビルドするためにNodeの環境を追加します。
今回はWebpackやBrowserifyの代わりにParcel bundlerを使ってみたいと思います。「webpack時代の終わりとparcel時代のはじまり - Qiita」に触発されました。
まずはRAILS_ROOT直下にreactディレクトリを作成し下記の様なファイルを設置します。
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
{
"presets": ["env", "react"]
}
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" ]
コマンドで空ファイルなどを作成
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のモジュールをインストール
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
に追加してください。
<!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に書きます。
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で復号してください。
<%= set_meta_tags(
params: {
foo: @foo_,
bar: @bar_
}.to_json
) %>
<div id="container">Loading</div>