Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

(このエントリーは、エキサイト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つ。

  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

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.jsapp.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はアプリケーション固有のconfigureStoreApp コンポーネントを指定します。

デモ

デモを動かしてみます。(要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

スクリーンショット 2016-12-01 13.17.04.png

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

スクリーンショット 2016-12-01 13.17.59.png

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についての記事です。お楽しみに。

bengo4
「専門家をもっと身近に」を理念として、人々と専門家をつなぐポータルサイト「弁護士ドットコム」「弁護士ドットコムニュース」「税理士ドットコム」を提供。
https://corporate.bengo4.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away