Single Page Applicationの実装を体験して

# なぜSPAを実装しようと思ったか
これまでの案件の進め方でフロントエンドとバックエンドをそれぞれで実装していました（分業制であったため）。

ただ、こういう構図になると必ず発生するのがそれぞれで管理（ルーティングや認証）を行う必要があり、無駄な工数が掛かっているなとすごく感じてました。
そうすると、必然的にWEB APIが必要になりますので一緒にしちゃった方が楽だし、Laravelでフロントエンド機能があるというのを知り、どうせなら実際に実装してみた方が良いかなと思って取り組み始めました。

## なぜinertia.jsなのか？
前述したようにフロントエンドとバックエンドの実装となるとWEB APIが必須となるでしょう。
それってすごく無駄に感じますね。
バックエンドで取得した情報をそのままビューで使えればそれが一番シンプルだろ！と思うのは普通ですよね。
それを叶えるのが、<b>inertia.js</b>でしたね。
なので、すんなりと決まりました。

## 開発環境
下記の構成でアプリケーションを実装しました。
これまでAWSで無料期間中に利用していましたが、料金もそこそこ高くさくらインターネットへ移行することを決意。
<p>
    <ul>
        <li>サーバー：さくらインターネット</li>
        <li>フロントエンド：Inertia.js</li>
        <li>プログラム言語：PHP 8.4</li>
        <li>データベース：MySQL 8.0</li>
        <li>フレームワーク：Laravel 12</li>
    </ul>
</p>
<p style="display: inline">
    <img src="https://img.shields.io/badge/-TailwindCSS-000000.svg?logo=tailwindcss&style=for-the-badge">
    <img src="https://img.shields.io/badge/-Php-777BB4.svg?logo=php&style=for-the-badge&logoColor=white?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=5d58a113c666929bd6e51d1dd4efb231">
    <img src="https://img.shields.io/badge/-MySQL-4479A1.svg?logo=mysql&style=for-the-badge&logoColor=white?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=4d494b1e1970d495620c420e66467c57">
    <img src="https://img.shields.io/badge/-Laravel-CC0000.svg?style=for-the-badge&logo=laravel&logoColor=white?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=364c32f5399c14802bb1f93ef6813687">
</p>

## 実装内容

### inertiaへ切り替え
全てのページをinertiaでレスポンスするように設定としました。
例外処理を設定時にバリデーションエラーはデフォルトのままなど、動かして気づくことが多々ありましたが。
例外処理の内容についてはもっとスマートな情報があったかも知れません。。

<details>

<summary>bootstrap.phpの設定</summary>

``` PHP: bootstrap/app.php

<?php

use App\Enums\ApiExceptionMessage;
use App\Http\Middleware\CustomSetCacheHeaders;
use App\Http\Middleware\ShareInertiaAuthUser;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Middleware\SetCacheHeaders;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\UnauthorizedException;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->web(append: [
            \App\Http\Middleware\HandleInertiaRequests::class,
            \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
            \App\Http\Middleware\ShareInertiaAuthUser::class
        ]);

        $middleware->replace(SetCacheHeaders::class, CustomSetCacheHeaders::class);
        $middleware->redirectGuestsTo(fn () => route('login.index'));
        $middleware->alias([
            'custom_guest' => \App\Http\Middleware\CustomRedirectIfAuthenticated::class,
            'no_team' => \App\Http\Middleware\RedirectIfDoesntHasTeam::class,
            'has_team' => \App\Http\Middleware\RedirectIfHasTeam::class,
            'optional.auth' => \App\Http\Middleware\OptionalAuthenticate::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        $exceptions->render(function (Exception $e, Request $request) {
            // バリデーションエラー時はLaravelのロジックで処理する
            if ($e instanceof \Illuminate\Validation\ValidationException) {
                return null;
            }

            // 認証エラー時はLaravelのロジックで処理する
            if ($e instanceof \Illuminate\Auth\AuthenticationException) {
                return null;
            }

            $status = 500;

            if ($e instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface) {
                $status = $e->getStatusCode();
            }

            $errorPage = match ($status) {
                403 => 'Errors/403',
                404 => 'Errors/404',
                405 => 'Errors/405',
                500 => 'Errors/500',
                default => 'Errors/500',
            };

            return inertia($errorPage)->toResponse($request)->setStatusCode($status);
        });
    })->create();


```

</details>

### ビューの呼び出し
最低限必要な情報のみコントローラからビューに渡しました。
ビューでURL生成メソッドなど利用できましたが、一元管理をしたかったので、受け取って表示するだけの構成にしています。
※blade.phpのようにroute()メソッドをビュー内で利用可能

<details >

<summary>コントローラの処理</summary>

``` PHP: xxxController.php
    public function index(): Response
    {
        return Inertia::render('Register/TeamRegistrationForm', [
            'prefectures' => Prefecture::list(),
            'sports' => SportAffiliationTypeEnum::list(),
            'url' => [
                'confirm' => route('register.team.confirm'),
            ]
        ]);
    }
```
</details>

### 悩んだこと
#### 1. 認証済みと未認証でメニューの切り替え

理由は、通常authミドルウェアを指定したルートに適用すると、認証されていないとログイン画面にリダイレクトする仕様となっています。
これを避けるためにミドルウェアを適用しないと、そもそも認証情報を扱わない状態となるためAuthファサードで情報を取得しようとしても、常にnullが返る状態となっていました。

強制的に対象ルートに対して認証用ミドルウェアを適用する実装w
こんな方法あるよなどあればぜひコメントほしいです！！

<details>

<summary><b>カスタム作成したミドルウェア</b></summary>

``` PHP: App\Http\Middleware\OptionalAuthenticate.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class OptionalAuthenticate
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {
        // 明示的に guard('user') を指定
        Auth::shouldUse('user');

        // セッションに認証済みユーザがいれば Auth::user() が使えるようになる
        return $next($request);
    }
}

```

</details>

#### 2. パフォーマンスが悪い
assets類の読み込みを全く考慮していなく、下記のサイトでパフォーマンスチェックをするとスマホ表示で６５％と出て速攻で修正入りました。
パフォーマンスの結果から、assetsの読み込み、圧縮などこれまで意識していなかったところを学べました。
gzipを使った圧縮、.htaccessで指定のmimeTypeを圧縮して読み込みできるの？と目から鱗でした。

- [PageSpeed Insights](https://pagespeed.web.dev/?hl=ja)

##### 参考サイト
- 引用元：[さくらサーバーGZIP圧縮を有効にすると「PageSpeed Insights」が100点になった！](https://qiita.com/mgasl/items/030efb4f8118a8102aab)