やりたいこと
rails + react + react-router (ゆくゆくは + redux)な環境でサーバサイドレンダリング可能なシングルページアプリケーションを作成したい。自分でも「何言ってんだお前」って感じだけど、やりたいんです。
react_on_rails
rails、react間のインテグレーションを実現するgemはreact-railsっていうのとreact_on_railsっていうのがある。react-railsの方が情報量が多かったけど、application.js
で
//= require react
//= require react_ujs
//= require components
ってぶちまける感じが好きじゃないのでちょっといじって投げ捨てました。そういうわけでreact_on_railsを使います。
gem 'react_on_rails', '~> 6'
インストールする。未コミットの差分があるとインストールできないので注意。
bundle exec rails g react_on_rails:install
コントローラにReactコンポーネントをレンダリングするメソッド生やす。
ここから拝借したコード。下記のコードはRouterコンポーネントをレンダリングする。まだそのコンポーネントを登録してないので後で{window,global}.ReactOnRails
に登録しないといけない。
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def home
render_for_react()
end
private
# ref: http://r7kamura.hatenablog.com/entry/2016/10/10/173610
def common_props
{
currentUser: current_user,
}
end
def render_for_react(props: {}, status: 200)
if request.format.json?
response.headers["Cache-Control"] = "no-cache, no-store"
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
response.headers["Pragma"] = "no-cache"
render(
json: common_props.merge(props),
status: status,
)
else
render(
html: view_context.react_component(
"Router",
prerender: true,
props: common_props.merge(props).as_json,
),
layout: true,
status: status,
)
end
end
end
npmを足しとく
# in client/
yarn add react-router react-helmet
{server,client}Registration.jsx
それぞれサーバサイドとブライザで実行するjsをwebpackでバンドルする際のエントリファイル。
react-router使わないならデフォルトで生成されるregistration.jsx
だけでいいんだけど、使う場合はそれぞれ別のRouterを用意する必要があった。この辺で頭がおかしくなってコードが釈迦になりそう。
こっちはブラウザ用のコード。
import React from 'react';
import { Router as ReactRouter, browserHistory } from 'react-router';
import ReactOnRails from 'react-on-rails';
import routes from '../routes/routes';
const Router = (props, railsContext) => {
const history = browserHistory;
return (
<ReactRouter history={history}>
{routes}
</ReactRouter>
);
};
// This is how react_on_rails can see the Router in the browser.
ReactOnRails.register({
Router,
});
こっちはサーバサイドレンダリング用のコード。routes.jsx
は両方でimportして共有している。
import React from 'react';
import { match, RouterContext } from 'react-router';
import ReactOnRails from 'react-on-rails';
import Helmet from 'react-helmet';
import routes from '../routes/routes';
// for header title server side rendeting on first load
// ref: http://r7kamura.hatenablog.com/entry/2016/10/10/173610
global.Helmet = Helmet;
const Router = (props, railsContext) => {
let error;
let redirectLocation;
let routeProps;
const { location } = railsContext;
match({ routes, location }, (_error, _redirectLocation, _routeProps) => {
error = _error;
redirectLocation = _redirectLocation;
routeProps = _routeProps;
});
if (error || redirectLocation) {
return { error, redirectLocation };
}
return (
<RouterContext {...routeProps} />
);
};
ReactOnRails.register({
Router,
});
webpackの設定も書き換えておく。
/**
* from
*/
entry: [
'es5-shim/es5-shim',
'es5-shim/es5-sham',
'babel-polyfill',
'./app/bundles/Application/startup/registration',
],
output: {
filename: 'webpack-bundle.js',
path: '../app/assets/webpack',
},
/**
* to
*/
entry: {
vendor: [
'es5-shim/es5-shim',
'es5-shim/es5-sham',
'babel-polyfill',
],
app: [
'./app/bundles/Application/startup/clientRegistration',
],
server: [
'./app/bundles/Application/startup/serverRegistration',
]
},
output: {
filename: '[name]-bundle.js',
path: '../app/assets/webpack',
},
これでvendor-bundle.js
、app-bundle.js
、server-bundle.js
の3つが出力されるようになった。さらに、react_on_railsの設定ファイルも書き換えておく。さっき分けたサーバサイド用のjsファイルをExecJSで実行するように指定する。
# from
config.webpack_generated_files = %w( webpack-bundle.js )
# to
config.webpack_generated_files = %w( app-bundle.js server-bundle.js vendor-bundle.js )
# from
config.server_bundle_js_file = "webpack-bundle.js"
# to
config.server_bundle_js_file = "server-bundle.js"
turbolinksを殺す
react-routerでトランジションするSPAという状況下で、いらない子になったturbolinksを殺す。さようなら。お前に苦しめられた日々、忘れないよ。
Universalの苦しみ、こんにちは。
//= require vendor-bundle
//= require app-bundle
//= require jquery
//= require jquery_ujs
もちろんGemfile
からも抹殺する。それからlayout
を書き換える。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<%= server_render_js("Helmet.rewind().title.toString()") %>
<%= csrf_meta_tags %>
<%= stylesheet_link_tag 'application', media: 'all' %>
</head>
<body>
<%= yield %>
<%= javascript_include_tag 'application' %>
</body>
</html>
起動
# in app root dir
foreman start -f Procfile.dev
ブラウザのインスペクタを開いてconsoleにエラー吐いてなかったら多分大丈夫。ちゃんとなってればreact-router
のLink
コンポーネントで遷移した時と、URL直だたきのときで全く同じページが表示されるはず。
違うページが表示されたりエラーが出る場合は、だいたいcontrollerを作ってなかったとか、routes.rb
でマウントし忘れてたとかいうオチ。
参考になったページとか
- Using React Router | react on rails
- react-webpack-rails-tutorial
- Ruby on Rails on React on SSR on SPA | r7kamura
それなりに動くようになったもの
今後やりたいこと
- 今のところdeviseがSPA構成をガン無視していてつらい。コントローラとかビューをreact_componentヘルパ向けに書き直す必要がありそう。
- ブラウザに配信するjsの出力先を/publicにしてsproketsを使わないようにする。jqueryとかもnpmを使う。
- jbuilderを投げ捨ててrails-apiが搭載のactive_model_serializersをpropsのシリアライズに使いたい。
- テストの知見を貯めたい。