Edited at

ReactとDockerで既存のRailsアプリケーションをちょっとモダンに作り変える

More than 1 year has passed since last update.


TL;DR

タイトルの通り


はじめに

今年度に入ってからはReact Nativeでアプリを作っていたのですが、

7月からRailsで作られているサービスに携わることになったので、現在Railsの勉強中です。

いざ触って見ると、


  • 環境構築が面倒

  • バージョンが古いものが若干混じってる

  • erbとかhamlとかなんか苦手

  • Reactが書きたい

  • コントローラがややこしい

  • すぐに仕様は追加される

  • 不必要にレンダリングされるのが嫌だ

  • _formとかindexとか便利か?

などとワガママや偏見を感じてきたので、

一旦、RailsをDockerに乗せ、react-railsを使ってフロントをReactに書き変えていこうと思います。

(Docker化はワガママではなく、当初の予定通り)

将来的にはフロントは普通のReactに完全移行して、バックエンドも必要に応じて良しなに書き換えていきたいです。


目次


  • Docker化

  • Reactの準備

  • CRUDの確認


Docker化

(Dockerを使わない人は、多分飛ばして大丈夫です。)

元々は普通のrubyのimageを使っていたのですが、imageサイズが1gbを超えてしまったので

なんとなく小さくしようと思い、imageはruby:2.5.1-alpineを使いました。(会社のやつはrubyのままです)

alpineにrubyを入れた方が小さくできるのかもしれませんが、

270mbまで落ち着いたので、まぁ良さそうです。

FROM ruby:2.5.1-alpine

WORKDIR /awesome_app
COPY Gemfile* /awesome_app/

RUN apk upgrade --no-cache && \
apk add --update --no-cache \
postgresql-client \
nodejs \
yarn \
tzdata && \
apk add --update --no-cache --virtual=build-dependencies \
build-base \
linux-headers \
libxml2-dev \
libxslt-dev \
postgresql-dev \
ruby-dev \
yaml-dev \
zlib-dev && \
gem install bundler && \
bundle install -j4 && \
apk del build-dependencies --purge && \
rm -rf /tmp/src && \
rm -rf /var/cache/apk/

RUN npm install yarn -g && yarn install && gem update bundler && bundle install;

alpineはbash等、色々と足りてない様ですが、慣れればなんとかなると思います。

足りないものがあれば、apkで適宜追加して凌ぎます。

docker-compose.yamlは下のような感じになりました。


docker-compose.yaml

version: '3.5'

services:
postgres:
image: postgres:10.4
environment:
TZ: Asia/Tokyo
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- 5432:5432
networks:
- docker-network
volumes:
- postgres-data:/var/lib/postgresql/data
stop_signal: SIGINT
restart: always

webpacker:
build: '../awesome_app'
command: ash -c 'rm -rf /awesome_app/public/packs; /awesome_app/bin/webpack-dev-server'
volumes:
- ../awesome_app:/awesome_app
ports:
- 3035:3035
networks:
- docker-network
restart: always

web:
build: '../awesome_app'
command: ash -c 'rm /awesome_app/tmp/pids/server.pid; bundle exec rails s'
image: rails:dev
volumes:
- ../awesome_app:/awesome_app
environment:
TZ: Asia/Tokyo
networks:
- docker-network
ports:
- 3000:3000
depends_on:
- webpacker
- postgres
restart: always

networks:
docker-network:
volumes:
postgres-data:


開発環境ではwebpack-dev-serverを立てて、Reactの部分を表示させます。

Dockerを使わない場合は、デフォルトのままで良いのですが、今回はDockerを使うことにしたので

config/environments/develop.rbとconfig/webpacker.ymlを編集する必要があります。


develop.rb

  config.x.webpacker[:dev_server_host] = "http://webpacker:3035"



webpacker.yml

  dev_server:

https: false
host: webpacker
port: 3035
public: 0.0.0.0:3035
hmr: true
# Inline should be set to true if using HMR
inline: true
overlay: true
compress: true
disable_host_check: true
use_local_ip: false
quiet: false
headers:
'Access-Control-Allow-Origin': '*'
watch_options:
ignored: /node_modules/

これでDocker環境は完成です。


Reactの準備

次に、Reactの準備です。react-railswebpackerを使っていきます。

インストール方法などは以下の記事が参考になりました。

- https://qiita.com/kouheiszk/items/c85e70e331ba75841818

- https://qiita.com/joe-re/items/96f12dda4a62470d1d7c

react-railsとwebpackerは、デフォルトではapp/javascript/packs以下のファイルを参照しているようです。

webpackerに関しては、config/webpacker.ymlで設定を変更できるのですが、

react-railsの方がどこで設定を上書きすれば良いのか分からなかったので、そのままにしています。

そして、app/javascript/packs/application.jsを


app/javascript/packs/application.js

// By default, this pack is loaded for server-side rendering.

// It must expose react_ujs as `ReactRailsUJS` and prepare a require context.
var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)

と書き換えることで、app/javascript/components/Users.tsxを.erbから

<%= react_component('Users') %>

の様に呼べる様です。

良くやる様に、

app/javascript/components/Users/

└── index.tsx

とした場合、index.tsxをちゃんと読んでくれるので、

適当にパーツごとに分けて開発を進めることも出来そうです。

Reactを書くとなると、ステートレスに書きたくなりますが、

react-railsではステートフルに書く方が簡単そうです。

ReactとRailsを完全に分離できたら、Reduxを入れつつステートレスに書き換えていけば良いかと思います。

これで、Rails特有の_form, index, newなどのファイルが不要に出来、

また、機能が全てReact側に移れば、erbファイルに記述するものも1行で済みます。

users/

└── index.html.erb


Users.erb

<%= react_component('Users') %>


フォーム登録などは別ページで行った方が良い場合もあると思うので、

その場合はerbを複数用意したまま、それぞれをReactに移していけば良いかと思います。


CRUDの確認

最後にReactからaxiosでCRUDをする方法を書いていきます。

Railsはjsonを返すapiサーバとして使える様で、コントローラ側はあまり苦労はしないかと思います。

また、axiosでpost, get, put, deleteするだけなのでフロント側もそこまで苦労はしなさそうです。

CSRFの設定は、このままだと少し引っかかるので、そこだけ書いておきます。

デフォルトではRailsの方でCSRF対策をしてくれているので、

React側からaxiosでReadは出来てもCreateなどは出来ません。

特にセキュリティなどには気を使わなくていいアプリケーションであれば、application_controller.rbに


application_controller.rb

protect_from_forgery with: :null_session


と追記すれば動いてくれる様です。

今回は、react-on-railsを使ってaxiosのheaderにCSRF用のトークンを埋め込みます。

(最初に見たときはreact-railsとreact-on-railsのどちらも大差なく感じたので

githubの星が多いreact-railsを使っていたのですが

今後もこの手の細かいところで差が出てくるかもしれません)

import axios from "axios";

//@ts-ignore
import ReactOnRails from "react-on-rails";

axios.defaults.withCredentials = true;

axios.interceptors.request.use(
config => {
config.headers["X-CSRF-TOKEN"] = ReactOnRails.authenticityToken();
return config;
},
error => {
return Promise.reject(error);
}
);

これで、DBへのCRUDもできる様になりました。


終わり

これで少しずつerbをReactに入れ替えていく事が出来るハズです。

(viewのファイルの数に比例して手間ですが。。。)

ゼロから作るならRailsとReactは別で作った方が圧倒的に作りやすいかと思いますが、

既存のアプリケーションを移行するときは、この様な形で少しずつ行う方が安全かと思います。

流石に一ヶ月もRailsを触っていると、ある程度慣れてきたので、hamlでもそこまで困らなくなってきたのですが

仕様変更・機能追加に柔軟に対応しようとすると、フロントである程度綺麗に整形したものを

Railsに渡す方が開発しやすそうなので、ちょっと頑張ります。


TODO


  • nested_formなどRails特有の諸々が使えるのかを調べる

  • セッション等の取り扱いを調べる

  • 今のサービスのボトルネックになっているものはデータベースの様なので、そっちも調べる