PHPアプリケーションでのReact.jsのサーバーレンダリング考える

PHPでReact.jsなアプリケーションを導入する時に眺めてみた時のメモ。

まず、ブラウザからのリクエストに対して、サーバー側でDOMをレンダリングする必要がある。
これは、ブラウザでDOMレンダリングする場合は、サーチエンジンのアクセスに対して、
空の要素を返してしまう。
そのため、その結果がインデックスされるとSEO的によろしくないからである。

そこで、どこでReact.jsを実行してDOMを生成するのかを考える必要がある。

  • PHPでJSを実行する
  • React.jsをレンダリングする小さなアプリケーションに処理を委譲する

の2つのパターンを考えられる。

例として簡単なView Classのテンプレートをレンダリングする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React from 'react';

export default class Counter extends React.Component {
constructor(props) {
super(props);
this.countUp = this.countUp.bind(this);
this.countDown = this.countDown.bind(this);
this.state = {
count: this.props.count
}
};

static defaultProps = {
count: 0
};

static propTypes = {
count: React.PropTypes.number.isRequired
};

countUp(event) {
this.setState({count: this.state.count + 1});
};

countDown(event) {
this.setState({count: this.state.count - 1});
};

render() {
return(
<div className='counter'>
<span className='count'>
{this.state.count}
</span>
<button onClick={this.countUp.bind(this)}> count up </button>
<button onClick={this.countDown.bind(this)}> count down </button>
</div>
);
};
}

PHPでJSを実行する

PHPにV8jsモジュールを追加することで、
jsをコンパイルして実行できるようになる。
(require: PHP >= 5.3.3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# install v8js
pecl install v8js-0.1.3 \
echo "extension=v8js.so" >> /etc/php5/cli/php.ini

# install composer
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# オプションを追加してconfigure
./configure --with-freetype-dir=/usr/include/ \
--with-jpeg-dir=/usr/include/ \
--with-gd \
--with-mcrypt \
--with-libzip \
--with-bz2 \
--enable-mbstring \
--enable-v8js

通常ブラウザ向けにReact.jsを提供するのと同じように、
Browserifyを使いbundleし、v8jsでphpを実行する。

最終的にrenderToStringでreactIdを割り振られたDOMを出力する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require_once dirname(__FILE__) . '/../vendor/autoload.php';
function get_markup($component, $prop) {
$default_prop = json_encode($prop);
$v8 = new V8Js();
$js[] = "var global = global || this, self = self || this, window = window || this;";
$js[] = file_get_contents('js/bundle.min.js', true);
$js[] = "print(ReactDomServer.renderToString(React.createElement(${component}, ${default_prop})));";
$code = implode(";\n", $js);
ob_start();
$v8->executeString($code);
return ob_get_clean();
}
$component = 'Counter';
$prop = ['count' => 3];
$markup = get_markup($component, $prop);

Facebookのreact-php-v8jsをそのままに処理を書いた。

PHPにモジュールを追加できるのであれば、PHP側でtemplateを返すほうが容易である。
とくにreact-php-v8jsなどを使うことで簡単に実装はできそう。

テンプレート処理をレンダリングアプリケーションに委譲する

稼動中でPHPアプリケーションに対して手を入れることができない場合などは、
このパターンを採用することが考えられる。
実際にReact.jsを実行するのはnodejsで書かれたサーバーになる。

PHPアプリケーションからはHTTPリクエストを送信するだけとなる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$context = stream_context_create(
array(
'http' => array(
'method'=> 'POST',
'header'=> 'Content-type: application/json; charset=UTF-8',
'content' => json_encode(
array(
'count' => 4
)
)
)
)
);
$output = file_get_contents($url, false, $context);

という感じでPHPからPOSTされたdataに対して、
たとえばexpressでざっくりサーバーを書く。

こちらも最終的にrenderToStringでreactIdを割り振られたDOMを出力する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from 'react';
import ReactDOM from 'react-dom';
import express from 'express';
import path from 'path';
import ReactDOMServer from 'react-dom/server';
import bodyParser from 'body-parser';

const app = express();
app.use(bodyParser.json());

app.post('/', (req, res) => {
try {
const view = path.resolve('./views/' + req.query.component);
const Component = require(view).default;
const props = req.body || null;
res.status(200).send(
ReactDOMServer.renderToString(
React.createElement(Component, props)
)
);
} catch (error) {
res.status(500).send(error.message);
}
});

app.listen(3000, () => {
console.log('App listening on port 3000!');
});

こうすることでテンプレートを処理をして、htmlを返すことだけに注力できる。
nodejsサーバーからのレスポンスをPHPのレスポンスとして出力してあげると良い。

1
2
3
curl http://APPLICATION_IP/template_nodejs.php

<div id="app"><div class="counter" data-reactroot="" data-reactid="1" data-react-checksum="685065078"><span class="count" data-reactid="2">4</span><button data-reactid="3"> count up </button><button data-reactid="4"> count down </button></div></div>

仮にnodejsサーバーとPHPアプリケーションでのConnectionエラーなどがあった場合でも、
ブラウザレンダリング機構を用いれば、コンテンツが表示されないという致命的な問題は回避できる。

nodejsサーバーを採用した場合は、サーバーは常にステートレス(状態を保持しない)なので、
スケールアウトしやすく、またアプリケーションのテストも容易になる。

そのほか

どちらの方法採用しても、サーバーでレンダリングされたDOMに対して、
ブラウザ側でイベントをbindする必要がある。
この場合、すでにDOMがレンダリングされているので、React.jsはDOMを再描画する必要はない。

サーバーレンダリングを行うことで、ブラウザ側での描画を避けることができため、
サーバーからドキュメントがダウンロードされるとコンテンツが表示される。
つまり描画までの時間を短縮できる。


今回試したサンプル

参考にしたページ

v8js
react-php-v8js

Comments