React.jsとserver-side rendering

  • 225
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

今回は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で書くことが多いと思うので簡単に導入することが出来ます。

というわけで簡単なサンプルを示しながらやっていきたいと思います。

説明用に最低限のコードを用意したのでこれを元に説明していきます。

https://github.com/koba04/react-server-side-rendering-sample/tree/minimum

原則(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-jsxrenderToStringの部分です。
このサンプルでは/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に書きました。

http://qiita.com/koba04/items/e9de79b517662f3d9922

<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="[{&quot;id&quot;:1,&quot;name&quot;:&quot;backbone&quot;},{&quot;id&quot;:2,&quot;name&quot;:&quot;react&quot;},{&quot;id&quot;:3,&quot;name&quot;:&quot;angular&quot;}]"></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="[{&quot;id&quot;:1,&quot;name&quot;:&quot;backbone&quot;},{&quot;id&quot;:2,&quot;name&quot;:&quot;react&quot;},{&quot;id&quot;:3,&quot;name&quot;:&quot;angular&quot;}]"></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の方を使っているため注意が必要です。

https://github.com/reactjs/express-react-views

Node.js以外のサーバーで使う


というわけでReact.jsのserver-side renderingの仕組みについて簡単に書きました。
明日はReact.jsでのRoutingについて書きたいと思います。

この投稿は 一人React.js Advent Calendar 201417日目の記事です。