(このエントリーは、エキサイトAdvent Calendar 2016 の 12/02の2日目の記事です。エキサイト初の参加ということで私も参加しました。)
PHPのV8Jsを使ってRedux-ReactJsアプリケーションをサーバーサイドレンダリングするredux-react-ssrを紹介します。
用語
- V8Js
- SSR
- Redux
V8Js
V8JsはPHPの機能拡張でGoogle V8 JavaScript EngineをPHPに組み込みます。PHPから直接JSのコードが実行できます。
$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の略でクライアントではなくサーバーサイドでレンダリングを行うことをいいます。典型的なモチベーションは以下の2つ。
-
高速なページのロード。サーバーから完全なページが送られてくるのでデータフェッチのためのHTTPリクエストなどが不要で、キャッシュも可能です。
-
より信頼のできる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にインスパイアされた単方向データフローアプリケーションアーキテクチャのライブラリ」です。
Storeが持つアプリケーションの状態をReactJSがレンダリングします。ReactJsはビューだけを受け持ちます。
redux-react-ssr
ReactJSをSSRするライブラリとしてReact-PHP-V8Jsがあります。redux-react-ssrもこれと同様に機能します。違いはReactコンポーネントの状態を渡すのではなくReduxのStoreの状態を渡すところです。
<?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
を直接指定していますが、これは以下のようなものです。
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で宣言しています。
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と App コンポーネントを指定します。
デモ
デモを動かしてみます。(要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
クリックするとStateが変わり、サーバーサイドでレンダリングされたDOMがクライントサイドのJavaScriptでも引き継がれコントロールされていることが分かります。
HTMLを見るとと各タグがdata-reactid
というReactJsが管理するために必要なIDとサーバーサイドからクライアントサイドのJavaScriptに継続が可能か確認するためのdata-react-checksum
が付与されているのがわかります。
<!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
<!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はレンダリングされていませんが、ページの表示は変わりません。これはクライアントサイドでレンダリングされているためです。
リファレンス
明日は新人なのに山口百恵のカラオケが得意な牧山君のGoについての記事です。お楽しみに。