PHPのV8JsでSSR

[PHPのV8jsでReactJs-ReduxアプリをSSR](http://qiita.com/koriym/items/3a4e37dbe2d0d8e7dd60)からアップデート記事です。専用のユーティリティを使わずにV8JsでSSRをするときの基本的な方法を説明します。

## 用語

* SSR サーバーサイドレンダリング （V8)
* CSR クライアントサイドレンダリング (webブラウザ）

## サーバースクリプト

ReduxのSSRチュートリアルに載っているものとほぼ同じですが、`express`サーバーで出力するのではなく`PHP`からV8Jsで呼び出して出力します。PHP-JS間のインターフェイスとしてJS側の`render`関数をPHPに公開しています。

```index_ssr.bundle.js
import render from './render';
global.render = render;
```

```render.jsx
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import escape from 'escape-html';
import serialize from 'serialize-javascript';
import App from '../containers/App';
import configureStore from '../store/configureStore';

const render = (state, metas) => {
  const store = configureStore(preloadedState);
  const root = renderToString(
    <Provider store={store}>
      <App />
    </Provider>,
  );
  return `<!doctype html>
    <html>
      <head>
        <title>${escape(metas.title)}</title>
      </head>
      <body>
        <div id="root">${root}</div>
        <script>
          window.__PRELOADED_STATE__ = ${serialize(state)}
        </script>
        <script src="/build/index.bundle.js"></script>
      </body>
    </html>
`;
};

export default render;
```

PHPは「`render(state)`を実行するJSスクリプト」を文字列にして`executeString()`で実行します。

```php
$code = sprintf('
%s
var window = this;
render(%s);',
    file_get_contents(__DIR__ . "/ssr.bundle.js"),
    json_encode(['hello' =>['name' => 'World']])
);

echo (new V8Js)->executeString($code);
```
スクリプトの実行でSSRでレンダリングされる`${root}`の部分と`${serialize(state)}`は以下のようになります。DOMからhtmlが生成され、SSRからCSRに引き継ぐためのJSONが作られます。

```html
<div id="root"><div data-reactroot="" data-reactid="1" data-react-checksum="410796212"><h1 data-reactid="2"><!-- react-text: 3 -->Hello <!-- /react-text --><!-- react-text: 4 -->World<!-- /react-text --></h1><button data-reactid="5">Click</button></div></div>
```
```javascript
window.__PRELOADED_STATE__ = {"hello":{"name":"World"}}
```

## クライアントスクリプト

クライアントでは特別なことはありません。SSRでレンダリングされた`window.__PRELOADED_STATE__`の値からDOMをレンダリングしてドキュメントルートにセットします。

```index.bundle.js
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from '../store/configureStore';
import App from '../containers/App';

const preloadedState = window.__PRELOADED_STATE__;
const store = configureStore(preloadedState);

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);
```

## SSRは必要

Twitter CardsやOGPのmetaタグを生成する場合にはDOMを作るただけでは不十分でSSRが必要です。

 * [Reactで作ったページにTwitterCardsとOGPのメタデータを埋める](http://sambaiz.net/article/2/)
 * [You might need Server-side Rendering](https://medium.com/@andrewingram/you-might-need-server-side-rendering-99d252597dfa#.1kp9kzi5u)

## 汎用化

この記事の方法では(前回記事やfacebook公式の[react-php-v8js](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0ahUKEwjY2f7KoYLSAhWCgrwKHVSDAbMQFggcMAA&url=https%3A%2F%2Fgithub.com%2Freactjs%2Freact-php-v8js&usg=AFQjCNE6XDWOnoHwrrsmYE749auw-E2nFw&sig2=2Le_K6DjRSNmMPWXh_d8eg&cad=rjt)と違って)PHPからはJSの特定のライブラリに依存することなく、ページ全体をV8Jsで`render`関数を呼び出すことでレンダリングしています。

```javascript
global.render = (state, metas) => ('<html>..</html>');
```

`state`引数はアプリケーションの初期状態でReduxのSSRでの`preloadState`に使ったりテンプレートエンジンに渡したりする値です。

`metas`はSSRのみで使う値で主に、OGPやtitleなどheaderタグ内で使います。 

既存のJSアプリがある場合は、この`render`関数の形に適応させたwebpackのエントリーポイントにします。引数の名前は自由ですがPHPから呼び出すために`render`という名前は固定にします。

PHPとJSのインターフェイスを疎にしたしたことで、ビューの部分の入れ替えは容易になります。

## V8Jsのパフォーマンス

PHPでSSRをするには今回紹介している**V8Js**の方法と**Node.js**で[`Express`サーバーをたて`Guzzle`などのhttpクライアントからDOMをWebサービスと取得する方法](https://blog.frankdejonge.nl/rendering-reactjs-templates-server-side/)がありますが、パフォーマンスは圧倒的にV8Jsが有利です。

さらにV8Jsにはスナップショットという機能が用意されていてライブラリを含むアプリケーションコードをbuit inコードとして扱いPHPとV8を高速に連携させることが可能です。詳しくは以下の記事をご覧ください。

 * [V8JsのスナップショットでSSRを高速化](http://qiita.com/koriym/items/00b085fcb3b55bf338c6)

## 開発時にV8Jsを使わない方法

V8Jsの無い環境で開発する場合には[nacmartin/phpexecjs](https://github.com/nacmartin/phpexecjs)を使用することでSSRが可能です。環境に応じて`V8Js`または`Node.Js`のランタイムが選択され実行されます。

```php
use Nacmartin\PhpExecJs\PhpExecJs;

$phpexecjs = new PhpExecJs();
print_r($phpexecjs->evalJs("'red yellow blue'.split(' ')"));

// Array
// (
//     [0] => red
//     [1] => yellow
//     [2] => blue
// )
// READMEより
```

## SSRユーティリティ - Baracoa

[koriym/baracoa](https://github.com/koriym/Koriym.Baracoa) はこのようなJSアプリケーションをPHPからテンプレートエンジンのように扱うライブラリです。

```php
use Koriym\Baracoa\ExceptionHandler;
use Koriym\Baracoa\Baracoa;

$baracoa = new Baracoa($jsDir, new ExceptionHandler());
$html = $baracoa->render('min', ['name' => 'World']);
echo $html; // Hello World
```

```min.bundle.js
const render = state => (
  `Hello ${state.name}`
)
```

`phpexecjs`が使われてるのでV8Jsの無い環境でも実行可能です。
V8スナップショットの機能も統合されています。

```php
$cache = new FilesystemCache() // PSR-16 simple cache
$baracoa = new CacheBaracoa($appBundleJsPath, new ExceptionHandler(), $cache);
$html = $baracoa->render('min', ['name' => 'World']);
echo $html; // Hello World
```

## UIスケルトン

SSRのJSアプリケーションを開発する時の問題は環境構築やデバックが容易ではないことです。[koriym/js-ui-skeleton](https://github.com/koriym/Koriym.JsUiSkeleton)はJSの開発をすぐに始めらるためのボイラプレートとphpからのテスト実行環境を提供します。サーバーサイドのコードをクライアントで実行してトレースすることもできます。

 * airbnbリントで書かれたRedux Reactアプリケーションスケルトン
 * Webpack 2 / hot module loader 3など最新のツール
 * phpビルトインサーバー + ブラウザシンク
 * render()関数
 
新規プロジェクトとしても、既存のPHPプロジェクトに追加する形でも利用できます。

## 結論

SSRが必要か不要かには議論があります。しかしCSR/SSRと分けてもそのほとんどのコードは共通のコードです。１つの方向性としてSSRの導入コストやメンテナンスコストを下げてSSRを利用しやすくするのが良いのではないかと思います。JSやPHPのツールを充実させ、手間を減らしてメリットを享受する方法はどうでしょうか。

一方PHPのプロジェクトの場合にSSRを`Node.js`で行うか`V8Js`で行うかについては議論はどうでしょう。よくメンテナンスされていて、オーバヘッドも少なく高速なV8Js採用でいいのではないでしょうか。