PHPのV8JsでSSR

  • 13
    いいね
  • 0
    コメント

PHPのV8jsでReactJs-ReduxアプリをSSRからアップデート記事です。専用のユーティリティを使わずに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()で実行します。

$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が作られます。

<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>
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が必要です。

汎用化

この記事の方法では(前回記事やfacebook公式のreact-php-v8jsと違って)PHPからはJSの特定のライブラリに依存することなく、ページ全体をV8Jsでrender関数を呼び出すことでレンダリングしています。

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.jsExpressサーバーをたてGuzzleなどのhttpクライアントからDOMをWebサービスと取得する方法がありますが、パフォーマンスは圧倒的にV8Jsが有利です。

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

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

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

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 はこのようなJSアプリケーションを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スナップショットの機能も統合されています。

$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はJSの開発をすぐに始めらるためのボイラプレートとphpからのテスト実行環境を提供します。サーバーサイドのコードをクライアントで実行してトレースすることもできます。

  • airbnbリントで書かれたRedux Reactアプリケーションスケルトン
  • Webpack 2 / hot module loader 3など最新のツール
  • phpビルトインサーバー + ブラウザシンク
  • render()関数

新規プロジェクトとしても、既存のPHPプロジェクトに追加する形でも利用できます。

結論

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

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