今回はReact.jsの大きな特徴の1つで、これが出来るから使うという人もいるserver-side renderingについて書きたいと思います。
server-side renderingとは文字どおりサーバーサイドでHTMLを生成してrendering出来るようにするものです。SinglePageApplicationのようなJavaScriptでDOMを組み立てるようなアプリケーションの場合、サーバーから返されるHTMLには空のdivだけがあってそこからJavaScriptを読み込んでtemplateを描画することになり、これには2点の問題点があります。
- 初期のロード時間
- HTMLが返されてJavaScriptを評価してそこからtemplateの表示になるので、サーバーサイドからHTMLが返される場合と比べて当然時間が掛かります。なので別途ローディングを見せるなどの工夫が必要になります。
- SEO
- 最近だとGoogleのクローラーがJavaScriptを解釈するようになったのでGoogleについては問題ないと言えるかもしれませんが、その他のクローラーに対応しようとするとPhantomJSを使ってレンダリングしたHTMLを返すなど力技なことをする必要が出てきます。
React.jsでserver-side rendering
React.jsは、実際のDOMに依存しなくてもComponentのVirtualDOMからHTMLとして出力するメソッドを持っています。
なのでnode.jsなどでサーバーを作って、その中でReact ComponentをHTMLにして返してあげることが出来ます。最近はbrowser側でもbrowserifyなどを使ってcommonJSのstyleで書くことが多いと思うので簡単に導入することが出来ます。
というわけで簡単なサンプルを示しながらやっていきたいと思います。
説明用に最低限のコードを用意したのでこれを元に説明していきます。
原則(renderToStringの場合)
server-side renderingでは、サーバー側でReact.renderToString
で生成したHTMLのDOM構造とブラウザ側でReact.render
して生成されたComponentのDOM構造が同じである必要があります。
これはHTMLに付与されるdata-react-checksum
という値を使って比較しています。
checksumが違う場合、
React attempted to use reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server.
というwarnが出てサーバーで生成したHTMLによるDOMは破棄されて再度生成されます。
ちなみにこのchecksumはHTMLを元にAdler-32で生成されています。
Component
Componentについては、あまり意識する必要はありませんがcomponentWillMount
はサーバー側でも呼ばれてcomponentDidMount
はブラウザ側でしか呼ばれないなどは認識しておく必要があります。
var React = require('react');
var App = React.createClass({
getInitialState() {
return {
message: "loading..."
};
},
componentDidMount() {
this.setState({ message: "welcome!" });
},
render() {
var list = this.props.data.map(obj => <li key={obj.id}>{obj.id}:{obj.name}</li>);
return (
<div>
<p>server-side rendering sample</p>
<p>{this.state.message}</p>
<ul>{list}</ul>
</div>
);
}
});
module.exports = App;
ここでは、Componentがロードが終わったらloading...というメッセージをwelcomeに変えるような実装をしています。
Server
サーバー側で注目すべき点はnode-jsx
とrenderToString
の部分です。
このサンプルでは/bundle.js
のアクセスでbrowserifyを実行して動的に生成していますが、実際の場合は事前に変換したbundle.jsを用意しておいた方がいいです。
var express = require('express'),
app = express(),
fs = require('fs'),
browserify = require('browserify'),
reactify = require('reactify'),
Handlebars = require('handlebars'),
React = require('react')
;
require('node-jsx').install({ harmony: true });
var App = require('./components/app');
var data = [
{ id: 1, name: 'backbone' },
{ id: 2, name: 'react' },
{ id: 3, name: 'angular' },
];
var template = Handlebars.compile(fs.readFileSync('./index.hbs').toString());
app.get('/', function(req, res) {
res.send(template({
initialData: JSON.stringify(data),
markup: React.renderToString(React.createElement(App, {data: data}))
}));
});
app.get('/bundle.js', function(req, res) {
res.setHeader('content-type', 'application/javascript');
browserify('./browser')
.transform({ harmony: true }, reactify)
.bundle()
.pipe(res)
;
});
var port = process.env.PORT || 5000;
console.log("listening..." + port);
app.listen(port);
node-jsx
まず、JSXで書かれたComponentをrequireする必要があるので、ここではrequire('node-jsx').install({ harmony: true })
でjsxのファイルもrequire出来るようにしています。(harmony optionも有効で)
renderToString
React.renderToString
にはReact.createElement
で作ったComponentを渡します。
初期読み込みで使うデータがあればこれもComponentにPropとして渡しておきます。
また渡したデータはクライアント側にも共有する必要があるので、templateで渡すために別途JSON.stringifyしてinitialDataとして渡しています。
template
ここでのポイントは{{{}}}
(エスケープしない)でrenderToStringでHTML化した文字列を渡していることと、scriptタグの属性値としてinitialDataを渡していることです。
初期データの渡し方は色々エントリを見ていると、<script id="a">{{{data}}}</script>
として渡しているだけのものもあったりしますが、それだとdata
の値をユーザーが操作出来る場合、XSS出来るので注意が必要です。この辺りは以前に別途Qiitaに書きました。
<html>
<head>
<title>React.js server-side rendering sample</title>
</head>
<body>
<div id="app">{{{markup}}}</div>
<script id="initial-data" type="text/plain" data-json="{{initialData}}"></script>
<script src="/bundle.js"></script>
</body>
Browser
ブラウザ側のエントリポイントでは、initialDataの値を取得してそれを使ってComponentを作成しているだけです。
var React = require('react'),
App = require('./components/app')
;
var data = JSON.parse(document.getElementById('initial-data').getAttribute('data-json'));
React.render(<App data={data} />, document.getElementById("app"));
生成されるソース
生成されるソースはこんな感じでrootの要素にdata-react-checksum
が、それぞれにdata-reactid
が指定されています。
この状態に対してブラウザ側でReact.render
を使ってComponentに紐付けると、checksumの確認をして問題なければ、このDOMはそのままにイベントリスナの登録だけを行います。
なのでserver-side renderingをする際はHTMLがサーバーから返されてからJavaScriptが評価されてイベントリスナの登録が行われるまではイベントに反応しないのでその点は注意が必要です。
<body>
<div id="app"><div data-reactid=".25wfuv5brb4" data-react-checksum="-1037109598"><p data-reactid=".25wfuv5brb4.0">server-side rendering sample</p><p data-reactid=".25wfuv5brb4.1">loading...</p><ul data-reactid=".25wfuv5brb4.2"><li data-reactid=".25wfuv5brb4.2.$1"><span data-reactid=".25wfuv5brb4.2.$1.0">1</span><span data-reactid=".25wfuv5brb4.2.$1.1">:</span><span data-reactid=".25wfuv5brb4.2.$1.2">backbone</span></li><li data-reactid=".25wfuv5brb4.2.$2"><span data-reactid=".25wfuv5brb4.2.$2.0">2</span><span data-reactid=".25wfuv5brb4.2.$2.1">:</span><span data-reactid=".25wfuv5brb4.2.$2.2">react</span></li><li data-reactid=".25wfuv5brb4.2.$3"><span data-reactid=".25wfuv5brb4.2.$3.0">3</span><span data-reactid=".25wfuv5brb4.2.$3.1">:</span><span data-reactid=".25wfuv5brb4.2.$3.2">angular</span></li></ul></div></div>
<script id="initial-data" type="text/plain" data-json="[{"id":1,"name":"backbone"},{"id":2,"name":"react"},{"id":3,"name":"angular"}]"></script>
<script src="/bundle.js"></script>
</body>
renderToStringとrenderToStaticMarkup
使い分けですが、renderToStaticMarkupではdata-reactid
などが一切付与されていないただのHTMLが返されるので静的なページとして出力したい場合に使います(あんまりないと思いますが...)。
renderToString
と同じようにrenderToStaticMarkup
でHTMLを返してブラウザ側でもComponentに紐付けることも出来るのですが、その場合renderToStaticMarkup
で返したHTMLは一切利用されず、ブラウザ側で再度HTMLを作ることになるので無駄があります。
renderToStaticMarkup
で出力されるHTML
<body>
<div id="app"><div><p>server-side rendering sample</p><p>loading...</p><ul><li>1:backbone</li><li>2:react</li><li>3:angular</li></ul></div></div>
<script id="initial-data" type="text/plain" data-json="[{"id":1,"name":"backbone"},{"id":2,"name":"react"},{"id":3,"name":"angular"}]"></script>
<script src="/bundle.js"></script>
</body>
Flux時の注意点
Fluxのところで書きますが、ComponentがシングルトンのStoreを持っているようなアプリケーションをそのままServer側で動かすとStoreが共有されてしまうので注意が必要です。その場合はRequest毎にStoreを作るなどする必要があります。
Routing
Routingしたいときはどうするの?と思う人もいるかもしれませんが、これはserver-side renderingをサポートしているライブラリを使えば可能で、明日はRoutingについて取り上げるのでそのときに書きます。
express-react-views
このライブラリはドキュメントにもありますが、renderToStaticMarkup
の方を使っているため注意が必要です。
Node.js以外のサーバーで使う
-
react-rails
- railsと組み合わせたいときに使うといいもので、ExecJSをComponentの部分は処理するみたいです。
- https://github.com/reactjs/react-rails
-
React.NET
-
react-python
-
react-php-v8js
というわけでReact.jsのserver-side renderingの仕組みについて簡単に書きました。
明日はReact.jsでのRoutingについて書きたいと思います。