PHPのV8jsでReactJs-ReduxアプリをSSR

(このエントリーは、[エキサイトAdvent Calendar 2016](http://qiita.com/advent-calendar/2016/excite) の 12/02の2日目の記事です。エキサイト初の参加ということで私も参加しました。)

PHPのV8Jsを使ってRedux-ReactJsアプリケーションをサーバーサイドレンダリングする[redux-react-ssr](https://github.com/koriym/Koriym.ReduxReactSsr)を紹介します。

## 用語

 * V8Js
 * SSR
 * Redux

## V8Js

[V8Js](http://php.net/manual/ja/book.v8js.php)はPHPの機能拡張で[Google V8 JavaScript Engine](https://ja.wikipedia.org/wiki/Google_V8_JavaScript_Engine)をPHPに組み込みます。PHPから直接JSのコードが実行できます。

```php
$v8 = new V8Js();

/* basic.js */
$js = <<< EOT
len = print('Hello' + ' ' + 'World!' + "\\n");
len;
EOT;

try {
  var_dump($v8->executeString($js, 'basic.js'));
} catch (V8JsException $e) {
  var_dump($e);
}

// Hello World!
// int(13)

// http://php.net/manual/en/v8js.examples.php より
```

## SSR

SSRとはServer Side Renderingの略でクライアントではなくサーバーサイドでレンダリングを行うことをいいます。典型的なモチベーションは以下の２つ。

1. 高速なページのロード。サーバーから完全なページが送られてくるのでデータフェッチのためのHTTPリクエストなどが不要で、キャッシュも可能です。

2. より信頼のできるSEO。JavaScriptによってレンダリングされる空のページではなく、コンテンツの伴った完全なページを用意してクローラーに与えます。(しかしクローラーの性能向上でこのメリットは薄れつつはあります）

SSRされたHTMLはReactJSに対応したタグが打たれ、状態も引き継がれるためクライアントサイドのRedux Reactで継続可能です。(そのためにはSSRでレンダリングされるDOMとCSR(Client Server Rendering)でレンダリングされるDOMが完全一致する必要があります。)

SSRとCSRは違うのものでどちらかが完全に優れてるというものでもありません。エントリーポイントが多く、キャッシュが有効なブラウズ主体のページにSSRが向いていると思います。一方Google Play MusicやGmailのようなSPAのアプリにはCSRが向いていると思います。


## Redux

GitHubの説明よるとReduxは「Fluxにインスパイアされた単方向データフローアプリケーションアーキテクチャのライブラリ」です。

![redux-unidir-ui-arch.jpg](https://qiita-image-store.s3.amazonaws.com/0/3940/1337b2a8-22a8-856e-0779-0ef42a5c16bd.jpeg)

**Store**が持つアプリケーションの状態をReactJSがレンダリングします。ReactJsはビューだけを受け持ちます。

* [UNIDIRECTIONAL USER INTERFACE ARCHITECTURES](http://staltz.com/unidirectional-user-interface-architectures.html)


## redux-react-ssr

ReactJSをSSRするライブラリとして[React-PHP-V8Js](https://github.com/reactjs/react-php-v8js)があります。**redux-react-ssr**もこれと同様に機能します。違いはReactコンポーネントの状態を渡すのではなくReduxのStoreの状態を渡すところです。

```php
<?php

use Koriym\ReduxReactSsr\ReduxReactJs;
use Koriym\ReduxReactSsr\ExceptionHandler;

$ssr = new ReduxReactJs(
    file_get_contents(__DIR__ . '/build/react.bundle.js'),
    file_get_contents(__DIR__ . '/build/app.bundle.js'),
    new ExceptionHandler(),
    new \V8Js()
);

$view = $ssr('App', ['hello' => ['message' => 'Hello, Redux!']], 'root');

$html = <<<EOT
<!DOCTYPE html>
<html>
  <head>
    <title></title>
  </head>
  <body>
    <div id="root">{$view->markup}</div>
    <script src="build/react.bundle.js"></script>
    <script src="build/app.bundle.js"></script>
    <script>{$view->js}</script>
  </body>
</html>
EOT;

echo $html;
```

`react.bundle.js`と`app.bundle.js`を直接指定していますが、これは以下のようなものです。

```react.bundle.js
import React from 'react';
import ReactDOM from 'react-dom';
import ReactDOMServer from 'react-dom/server';
import { Provider } from 'react-redux';

global.React = React;
global.ReactDOM = ReactDOM;
global.ReactDOMServer = ReactDOMServer;
global.Provider = Provider;
```

`react.bundle.js`はReactで必要な引数をglobalで宣言しています。


```app.js
import configureStore from '../common/store/configureStore';
import App from '../common/components/App';
import { Provider } from 'react-redux';

global.App = App;
global.Provider = Provider;
global.configureStore = configureStore;
```

`app.bundle.js`はアプリケーション固有の[configureStore](https://github.com/koriym/Koriym.ReduxReactSsr/blob/1.x/example/redux/common/store/configureStore.js)と [App](https://github.com/koriym/Koriym.ReduxReactSsr/blob/1.x/example/redux/common/components/App.js) コンポーネントを指定します。

## デモ

デモを動かしてみます。（要php7, v8js)

```
git clone git@github.com:koriym/Koriym.ReduxReactSsr.git
composer install
cd Koriym.ReduxReactSsr/example/redux
npm install
npm run build
npm start
```
<img width="253" alt="スクリーンショット 2016-12-01 13.17.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/3940/9628c5b3-4420-d938-4efa-6365e8c8ddcb.png">

クリックすると**State**が変わり、サーバーサイドでレンダリングされたDOMがクライントサイドのJavaScriptでも引き継がれコントロールされていることが分かります。

<img width="245" alt="スクリーンショット 2016-12-01 13.17.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/3940/58043a78-83de-38a2-7030-005b09c1207e.png">

HTMLを見るとと各タグが`data-reactid`というReactJsが管理するために必要なIDとサーバーサイドからクライアントサイドのJavaScriptに継続が可能か確認するための`data-react-checksum`が付与されているのがわかります。

```html
<!DOCTYPE html>
<html>
  <head>
    <title></title>
  </head>
  <body>
    <div id="root"><div data-reactroot="" data-reactid="1" data-react-checksum="466367079"><div data-reactid="2"><h1 data-reactid="3">Hello, Redux!</h1><button data-reactid="4">Click</button></div></div></div>
    <script src="build/react.bundle.js"></script>
    <script src="build/app.bundle.js"></script>
    <script>ReactDOM.render(React.createElement(Provider,{store:configureStore({"hello":{"message":"Hello, Redux!"}}) },React.createElement(App)),document.getElementById('root'));</script>
  </body>
</html>
```

次にSSRでレンダリングでマークアップされたHTMLを以下のように外して実行してみましょう。

**変更前**
`<div id="root">{$markup}</div>`

**変更後**
`<div id="root"></div>`

**レンダリングされたHTML**

```html
<!DOCTYPE html>
<html>
  <head>
    <title></title>
  </head>
  <body>
    <div id="root"></div>
    <script src="build/react.bundle.js"></script>
    <script src="build/app.bundle.js"></script>
    <script>ReactDOM.render(React.createElement(Provider,{store:configureStore({"hello":{"message":"Hello, Redux!"}}) },React.createElement(App)),document.getElementById('root'));</script>
  </body>
</html>

<img width="253" alt="スクリーンショット 2016-12-01 13.17.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/3940/2298ba96-e792-98ce-cefa-aa80a8b41ad1.png">
```

サーバーサイドでレンダリングされたHTMLはレンダリングされていませんが、ページの表示は変わりません。これはクライアントサイドでレンダリングされているためです。

## リファレンス

* [What are the tradeoffs of client-side rendering vs. server-side rendering?](https://www.quora.com/What-are-the-tradeoffs-of-client-side-rendering-vs-server-side-rendering)

明日は新人なのに山口百恵のカラオケが得意な牧山君のGoについての記事です。お楽しみに。