JavaScript SSR

ビューのレンダリングをTwigなどのPHPのテンプレートエンジンが行う代わりに、サーバーサイドのJavaScriptが実行します。PHP側は認証・認可・初期状態・APIの提供を行い、JavaScriptがUIをレンダリングします。既存のプロジェクトの構造で、アトリビュートが付与されたリソースのみに適用されるため、導入が容易です。

背景と適用場面

このモジュールは、PHPアプリケーション内でJavaScriptによるサーバーサイドレンダリング(SSR)を実現するために開発されました。

現在では、Next.js、Nuxt、Remixなど、JavaScriptエコシステム側でSSRを完結させるフレームワークが成熟しています。新規プロジェクトでJavaScript中心のUIを構築する場合、これらのフレームワークが第一選択となるでしょう。

本モジュールが適しているのは以下のようなケースです:

  • 大部分のページは既存のテンプレートエンジンでレンダリングし、高度なインタラクティブ性が求められる一部のページのみJS UIを使いたい
  • 既存のBEAR.Sundayプロジェクトに、特定のページのみReactやVue.jsのUIを追加したい
  • フロントエンドとバックエンドを分離せず、単一のPHPアプリケーションとして運用したい
  • PHPチームとJSチームがstate/metasを契約として並行開発したい

#[Ssr]アトリビュートを付与したリソースのみがJS UIでレンダリングされるため、従来のテンプレートエンジンとの共存が容易です。

前提条件

  • PHP 8.2以上
  • Node.js
  • V8Js(開発時はオプション)

注:V8Jsがインストールされていない場合、Node.jsでJavaScriptが実行されます。

JavaScript

インストール

プロジェクトにbear/ssr-moduleをインストールします:

# 新規プロジェクトの場合
# composer create-project bear/skeleton MyVendor.MyProject; cd MyVendor.MyProject
composer require bear/ssr-module

UIスケルトンアプリケーションkoriym/js-ui-skeletonをインストールします:

composer require koriym/js-ui-skeleton 1.x-dev
cp -r vendor/koriym/js-ui-skeleton/ui .
cp -r vendor/koriym/js-ui-skeleton/package.json .
npm install

UIアプリケーションの実行

まずはデモアプリケーションを動かしてみましょう。表示されたWebページからレンダリング方法を選択して、JavaScriptアプリケーションを実行します:

npm run ui

このアプリケーションの入力はui/dev/config/の設定ファイルで行います:

<?php
$app = 'index';                   // index.bundle.jsを指定
$state = [                        // アプリケーションステート
    'hello' => ['name' => 'World']
];
$metas = [                        // SSRでのみ必要な値
    'title' => 'page-title'
];

return [$app, $state, $metas];

設定ファイルをコピーして、入力値を変更してみましょう:

cp ui/dev/config/index.php ui/dev/config/myapp.php

ブラウザをリロードして新しい設定を試します。このように、JavaScriptや本体のPHPアプリケーションを変更せずに、UIのデータを変更して動作を確認することができます。

このセクションで編集したPHPの設定ファイルは、npm run uiで実行する時のみに使用されます。PHP側が必要とするのは、バンドルされて出力されたJavaScriptファイルのみです。

UIアプリケーションの作成

PHPから渡された引数を使ってレンダリングした文字列を返すrender関数を作成します:

const render = (state, metas) => (
    __AWESOME_UI__ // SSR対応のライブラリやJSのテンプレートエンジンを使って文字列を返す
);

stateはドキュメントルートに必要な値、metasはそれ以外の値(例えば<head>で使う値など)です。renderという関数名は固定です。

ここでは名前を受け取って挨拶を返す関数を作成します:

const render = state => (
    `Hello ${state.name}`
);

ui/src/page/hello/server.jsとして保存して、webpackのエントリーポイントをui/entry.jsに登録します:

module.exports = {
    hello: 'src/page/hello/server'
};

これでhello.bundle.jsというバンドルされたファイルが出力されるようになりました。

このhelloアプリケーションをテスト実行するためのファイルをui/dev/config/myapp.phpに作成します:

<?php
$app = 'hello';
$state = ['name' => 'World'];
$metas = [];

return [$app, $state, $metas];

以上です!ブラウザをリロードして試してください。

render関数内では、ReactやVue.jsなどのUIフレームワークを使ってリッチなUIを作成できます。

通常のアプリケーションでは、依存を最小限にするためにserver.jsエントリーファイルは以下のようにrenderモジュールを読み込むようにします:

import render from './render';
global.render = render;

ここまでPHP側の作業はありません。SSRのアプリケーション開発は、PHP開発と独立して行うことができます。

PHP

モジュールインストール

AppModuleにSsrModuleモジュールをインストールします:

<?php
use BEAR\SsrModule\SsrModule;

class AppModule extends AbstractAppModule
{
    protected function configure()
    {
        // ...
        $build = dirname(__DIR__, 2) . '/var/www/build';
        $this->install(new SsrModule($build));
    }
}

$buildフォルダはJavaScriptファイルがあるディレクトリです(ui/ui.config.jsで指定するwebpackの出力先)。

#[Ssr]アトリビュート

リソースをSSRするメソッドに#[Ssr]アトリビュートを付与します。appにJavaScriptアプリケーション名を指定する必要があります:

<?php
namespace MyVendor\MyRedux\Resource\Page;

use BEAR\Resource\ResourceObject;
use BEAR\SsrModule\Annotation\Ssr;

class Index extends ResourceObject
{
    #[Ssr(app: 'index_ssr')]
    public function onGet(string $name = 'BEAR.Sunday'): static
    {
        $this->body = [
            'hello' => ['name' => $name]
        ];
        return $this;
    }
}

$this->bodyrender関数の第1引数として渡されます。

CSRとSSRの値を区別して渡したい場合は、statemetasでbodyのキーを指定します:

#[Ssr(app: 'index_ssr', state: ['name', 'age'], metas: ['title'])]
public function onGet(): static
{
    $this->body = [
        'name' => 'World',
        'age' => 4.6E8,
        'title' => 'Age of the World'
    ];
    return $this;
}

実際にstatemetasをどのように渡してSSRを実現するかは、ui/src/page/index/serverのサンプルアプリケーションをご覧ください。

影響を受けるのはアトリビュートを付与したメソッドだけで、APIやHTMLのレンダリングの設定はそのままです。

PHPアプリケーションの実行設定

ui/ui.config.jsを編集して、publicにWeb公開ディレクトリを、buildにwebpackのビルド先を指定します。buildSsrModuleのインストール時に指定したディレクトリと同じにします:

const path = require('path');

module.exports = {
    public: path.join(__dirname, '../var/www'),
    build: path.join(__dirname, '../var/www/build')
};

PHPアプリケーションの実行

npm run dev

ライブアップデートで実行します。PHPファイルの変更があれば自動でリロードされ、Reactのコンポーネントに変更があれば、リロードなしでコンポーネントがアップデートされます。

ライブアップデートなしで実行する場合はnpm run startを実行します。

linttestなどの他のコマンドについては、コマンドをご覧ください。

パフォーマンス

V8のスナップショットをAPCuに保存する機能を使って、パフォーマンスの向上が可能です。ProdModuleApcSsrModuleをインストールしてください。V8Jsが必要です:

$bundleSrcBasePath = dirname(__DIR__, 2) . '/var/www/build';
$this->install(new ApcSsrModule($bundleSrcBasePath));

$bundleSrcBasePathはJavaScriptバンドルファイルがあるディレクトリのパスです。