こんにちは王子です。 最近は恐竜が闊歩するオープンワールドで過酷なサバイバルに挑むアクションゲーム「ARK: Survival Ascended」にハマっています。 旧Arkもひたすら遊んだので目新しさはありませんが、最初からIslandでプレイしてテリジノサウルスやアルゲンタヴィスをテイム&ライドするくらいまで進みました。
打倒!ブルードマザー!!
さて、ここ数日、SSR(Server-Side-Rendering)方式の実現方法について調査する機会がありました。 SPA/SSR/SSGについては本記事の主題ではありませんので、先に簡略な表現で記載すると下記のようになるでしょうか。
SPA(Single-Page-Application)
簡素なhtmlを先に受け取り、アプリ全体をscriptとして受け取る。アプリ構成は初回に受け取り済みのため、ページ遷移そのものはデータ通信を伴わない。SEOに弱い。ツール・業務システム向け。
SSR(Server-Side-Rendering)
httpのレスポンスとしてサーバー側で描画済みのhtmlを返す。画面で利用するJavaScriptは後追いで渡され(hyderateされ)、受け取り以降に利用できるようになる。SEOに強い。webサイト向け。
SSG(Server-Side-Generation)
事前にhtmlを出力しておく。他はSSRと変わらずか(hyderate時の挙動や適用範囲を調べたことがない)。私はこれをMovableTypeとつい呼んでしまう悪い癖があり、年齢がバレる気がしています。
背景
クイックではシステムの標準frameworkにLaravelを利用しています。
出来上がるシステムからすれば開発環境がなんであるかはあまり重要ではありませんが、開発で利用する様々な機能群(Migration/Seeder/Middlewareやテスト)やproduction環境におけるログ・死活監視などを考慮すると、どのようなものを作るせよ、弊害がない限り基本の開発環境をLaravelに寄せて、利用環境も似たような状況に寄せていきたいと考えています。
そうすることで新しい何かを作るときの準備時間や開発メンバーが異動するようなタイミングで一から色々と覚えなおす時間などのコストを低減できる見込みがあるからです。
SSR方式で開発を行う場合のframeworkを簡単に調べるとNext.js(or Nuxt)やSvelteなどが真っ先に出てくるでしょうか、少なくともLaravelは出てきません。そしてこういったframeworkではSSR時の描画にnodeでの待ち受けを前提としているため、開発環境だけでなくproductionの環境もnode前提の構成に変更していく必要があり、やや全体のコストが高い想定となりました。
そこで、LaravelやReact/Typescriptを前提としていくつかの環境を比較し、移行コストの低そうなSSR環境を探してみたのでその内容をブログ記事にしたいと思います。
やったこと
- 各環境の比較
- Laravel + Next.js
- Laravel + Vite
- Laravel + Inertia.js + Vite
- Laravel + Inertia.js + Viteでの環境構築
各環境の比較
- 同じ性質のものを横並びにしていないため、雰囲気での判定はご容赦ください
- 対応コストが高いものは暫定で不可と判定していますが、技術的には可能です
- 一般的に「技術的には可能」=「無理」と理解しています
Laravel + Next.js | Laravel + Vite | Laravel + Inertia.js + Vite | |
---|---|---|---|
DBのmigration | Laravel利用可 | Laravel利用可 | Laravel利用可 |
Seeder利用 | Laravel利用可 | Laravel利用可 | Laravel利用可 |
待ち受け | node | node | Laravel |
Middleware利用 | API待ち受けをLaravelにすれば、API側では可 | API待ち受けをLaravelにすれば、API側では可 | 可 |
所感 | Laravelにこだわる理由がない環境になる | 依存が少ない分、開発時の考慮が多い | フロント描画を除いてほぼ従来のLaravel利用と変わらない |
Laravel + Next.js
最も有名と思われるため、この選択で間違いないだろうと事前予想していましたが、環境を構築してみたところ、Laravelの介在価値が非常に低くなりました。
ネットの情報量は最も多く、開発メンバーがキャッチアップを行いやすいという魅力はありますし、複数の方式に対応していて優れたframeworkであると思うのですが、現時点ではクイックにとってproduction環境の構築コストが高いという無視できないネックがあります。
Laravelのことは忘れて、production環境をnodeを軸に構成し…と進めるならこちらでしょう。
Laravel + Vite
Laravel9あたりからビルドは基本Viteの初期設定となっています。このViteがビルド時に--ssrのオプションを利用することでSSR方式にも対応するようになっており、公式ドキュメントに記載とサンプルがあります。
ただし待ち受けがnodeのためLaravelの介在価値はNext.jsと同様に低く、上記のとおり最小構成のため、開発側で対応すべき項目がより多くなる結果となりました。
Laravel + Inertia.js + Vite
公式ドキュメントによるとクラシックなMVCとReact/Vueなどを結びつけるアダプターと記載があり、実際にそのような挙動になるので背景を考慮するとこれが最も適していそうです。
基本のweb待ち受けまでをLaravelで担当し、view表示タイミングでInertiaを介してhtmlの描画を行います。このタイミングでデータの受け渡しも行えるため、APIの作成も最低限で済みそうな気配です。従来のMVC開発の経験しかないメンバーにも親和性が高い構成になるとも思えました。
これら調査結果から、Laravel + Inertia.js + Viteに絞り、環境構築&色々な実装テストをおこなってみることにしました。
Laravel×React×Inertia×Viteの環境構築
公式ドキュメントにはBreeze/Jetstreamのスターターキットの案内がありますが、変更差分が追いにくくなるのでコンテナにマニュアルでセットアップしていきます。
ファイル構成
※laravelのインストールで出来るフォルダは省略 [root] [laravel-inertia-template-app] [docker] [web] apache-virtualhost.conf Dockerfile php.ini server.sh [routes] web.php [resources] [js] [Pages] First.tsx app.tsx ssr.js [views] app.blade.php .env package.json tsconfig.json vite.config.js
docker-compose.yml
- 他環境と衝突しないように、localhost:9190を80ポートとして利用
########################### # DBなどは無視して最低限の環境で services: ########################### # template_web : php + Apache + Composer + npm # webサーバー template_web: container_name: template_web build: ./laravel-inertia-template-app/docker/web env_file: - ./laravel-inertia-template-app/.env environment: - APP_TIMEOUT=60 - PHP_MEMORY_LIMIT=256M - PHP_MAX_UPLOAD=20M working_dir: /var/www/html/laravel-inertia-template-app ports: - "9190:80" # Laravel用 - "5173:5173" # Vite用 networks: - template_local volumes: - ./:/var/www/html networks: template_local: name: template_local driver: bridge driver_opts: com.docker.network.enable_ipv6: "false" ipam: driver: default config: - subnet: 172.16.246.0/24 # 他と衝突しなければなんでも良い認識
DockerFile & server.sh
- DockerFile
# use PHP official image FROM php:8.3.3-apache-bullseye # install pecl extensions RUN apt-get update \ && apt-get install -y libzip-dev unzip \ && docker-php-ext-install zip pdo mysqli pdo_mysql \ && docker-php-ext-install opcache \ && yes '' | pecl install redis # install composer RUN curl https://getcomposer.org/download/2.7.1/composer.phar -o /usr/local/bin/composer -s \ && chmod 755 /usr/local/bin/composer # install node.js RUN curl -sL https://deb.nodesource.com/setup_20.x | bash -\ && apt-get install -y nodejs # install git for local RUN apt-get update && \ apt-get install -y git # set configuration for apache and php COPY apache-virtualhost.conf /etc/apache2/sites-enabled/000-default.conf COPY php.ini /usr/local/etc/php/php.ini COPY server.sh /usr/local/bin/server.sh RUN chmod 755 /usr/local/bin/server.sh RUN ln -s /etc/apache2/mods-available/rewrite.load /etc/apache2/mods-enabled/rewrite.load # set entrypoint ENTRYPOINT ["/usr/local/bin/server.sh"]
server.sh
#!/bin/sh # get current user setting. USER=`grep apache /etc/passwd` # set user if user is not exist. if [ -z "$USER" ] && [ ! -z "$APACHE_UID" ]; then useradd -u $APACHE_UID apache mkdir -m 755 /home/apache chown apache /home/apache export APACHE_RUN_USER=apache fi # start apache. apache2-foreground wait
ここまででコンテナが立ち上がるように調整する .envやconfは用意しておかないと正常に起動しない
composer
- Laravelを入れたらInertiaだけ入れればOK
# コンテナ上で実施 # フォルダを一度空にする必要があるのでファイルを退避させてから composer create-project laravel/laravel . # inertiaのインストール composer require inertiajs/inertia-laravel
正常にインストールできていればlocalhost:9190へのアクセスでlaravelのwelcome画面が表示される
npm
- React/TypeScript関係とViteに関連するモノ
# コンテナ上で実施 # typescript/react関係 npm install --save-dev typescript react react-dom @types/react @types/react-dom @inertiajs/react # Vite関連 npm install --save-dev @vitejs/plugin-react
コード準備
- app.blade.php / メインの呼び出し元になるblade
- SPAのようなbladeになるが、これを元にSSRされる
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> @vite('resources/js/app.tsx') @inertiaHead </head> <body> @inertia </body> </html>
- middlewareの準備
# コンテナ上で下記を実行して
php artisan inertia:middleware
# App\Http\Kernelのwebに下記を追記
'web' => [
// ...
\App\Http\Middleware\HandleInertiaRequests::class,
],
- app.tsx / bladeから呼ぶコード
- ViteのHMRとSSR時で処理を分岐
- Pages以下のroutingに合致するコンポーネントを呼び出して処理する
import * as React from 'react'; import './bootstrap'; import { createInertiaApp } from '@inertiajs/react' import {createRoot, hydrateRoot} from 'react-dom/client' createInertiaApp({ resolve: name => { const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }) return pages[`./Pages/${name}.tsx`] }, setup({ el, App, props }) { if (import.meta.env.VITE_APP_ENV === "production") { hydrateRoot(el, <App {...props} />) } else { createRoot(el).render(<App {...props} />) } }, })
- ssr.js / ssr実行時のエントリポイント
- Pages以下のroutingに合致するコンポーネントを呼び出して処理する
import * as React from 'react'; import { createInertiaApp } from '@inertiajs/react' import createServer from '@inertiajs/react/server' import ReactDOMServer from 'react-dom/server' createServer(page => createInertiaApp({ page, render: ReactDOMServer.renderToString, resolve: name => { const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true }) return pages[`./Pages/${name}.tsx`] }, setup: ({ App, props }) => <App {...props} />, }), )
- vite.config.js
- ssrのinputを追加
- HMRのpollingは無茶しないように設定変更しているが必須ではない
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'], ssr: 'resources/js/ssr.jsx', refresh: true, }), ], server: { host: true, port: 5173, hmr: { host: 'localhost' }, watch: { usePolling: true, interval: 1000, } }, });
- tsconfig.son
- こちらはよしなに。ひとまず下記
{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true, "lib": [ "esnext", "dom" ], "types": [ "vite/client" ], "baseUrl": ".", "paths": { "@/*": [ "resources/*" ] } }, "include": [ "resources/**/*" ], "exclude": [ "node_modules" ] }
- package.json
- ビルド方式をSSRに変更する
{ "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build && vite build --ssr", "tsc": "tsc" }, ...(省略)
- web.php
- FirstのURLにアクセスがあった場合、Pages以下のFirst.tsxを参照する
- パラメータの受け渡しテストのため、適当な文字列や数値を設定
Route::get('/First', function () { return Inertia::render('First', [ 'numberValue' => 919, 'stringValue' => "Quick", 'arrayValue' => [ 11, 22, "33", 44, ] ]); });
- First.tsx
- 今回描画したいコンポーネント
import * as React from 'react'; interface IPros { numberValue: number; stringValue: string; arrayValue: number[]; } export const First: React.FC<IPros> = ({ numberValue, stringValue, arrayValue }) => { const handleClick = () => { console.log('クリックされた') } return ( <React.Fragment> <div> This is First Page. </div> <div> number: { numberValue } </div> <div> string: { stringValue } </div> {arrayValue.map(v => { return ( <div key={v}> {v} </div> ) })} <div style={{background: '#F00' }} onClick={() => handleClick()} > test </div> </React.Fragment> ) } export default First;
ローカル開発時のHMR
# ローカルでのHMRビルドを実施 npm run dev # 下記のような表示が出てくる LARAVEL v10.47.0 plugin v1.0.2 ➜ APP_URL: http://localhost
- 上記を実施したあとにhttp://localhost:9190/Firstにアクセスすると画像のようなページが表示される
- HMR状態なので、コードを変更すると即座に変更内容が反映された状態で再描画される
ビルドと待ち受け
# ビルド処理 npm run build # ビルド後、動作確認のためには下記のコマンドを実施する php artisan inertia:start-ssr # 下記のような表示が出てくる Starting SSR server on port 13714... Inertia SSR server started.
- 上記を実施したあとに
http://localhost:9190/First
にアクセスすると先程と同じ画面が表示される - SSRしているため、webブラウザのJavaScriptをoffにしても画面が表示される
- ※ボタンを押した時の処理は実施されない(hyderateされない)
最後に
まだ検証は続いていて、UIフレームワーク導入やフォームからのpost処理などを調査中です。 このあたりに問題がなければ、あとはAWS上に配置してみて監視などが行えつつ、大量アクセス時に特別な問題点がなければ上々と考えています。
以上ここまでお読みいただきありがとうございました。
\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //