AngularではじめるSSR入門

これは[Angular Advent Calendar 2023](https://qiita.com/advent-calendar/2023/angular)の15日目の記事です。

# はじめに

サーバーサイドレンダリング(SSR)って、なんか難しそうなイメージありますよね？
その割にメリットが少ないので、Angular Universalの存在は知っていたけど使ったことはない、という人は多いと思います。（自分もそうでした。）

そんな食わず嫌いの方々に向けて、本記事ではAngular SSRの環境構築からデプロイまでをさくっと紹介します。


# 前提条件

本記事の環境構築はこちらのバージョンで確認しています。

```zsh
$ ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 17.0.3
Node: 18.17.1
Package Manager: npm 9.8.1
OS: darwin arm64

Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1700.3 (cli-only)
@angular-devkit/core         17.0.3 (cli-only)
@angular-devkit/schematics   17.0.3 (cli-only)
@schematics/angular          17.0.3 (cli-only)
```


# 環境構築

新たにAngular SSRプロジェクトを作成します。プロジェクト名は`ng17-ssr`とします。

```zsh
$ ng new --ssr
? What name would you like to use for the new workspace and initial project? ng17-ssr
```

パッケージのインストールが終わったら、早速開発サーバーを起動してみます。

```zsh
$ npm run start
```

ブラウザを開いて`http://localhost:4200`にアクセスしてみると、新しくなったHelloページが表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187787/80af994b-ee2f-a03f-a578-cfac642c2910.png)

そして、なんとこのページはSSRされています！

ブラウザのJavaScriptを無効化しても同じように表示されるはずです。


# プリレンダリング(SSG)

プリレンダリングも簡単に確認できるので、やってみます。

実際は、この段階でも有効化されているのですが、わかりずらいので少し手を加えます。
具体的には、ランダムなUUIDを返してくれるAPIを使います。

https://www.uuidtools.com/

まず、`app.config.ts`に`HttpClient`を使うための設定を追加します。

```diff_typescript:app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { provideHttpClient } from '@angular/common/http';
import { provideClientHydration } from '@angular/platform-browser';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),
+   provideHttpClient(),
  ],
};

```

次に、`app.component.ts`でUUIDをAPIから取得して、表示させます。

```diff_typescript:app.component.ts
  import { CommonModule } from '@angular/common';
+ import { HttpClient } from '@angular/common/http';
+ import { Component, inject } from '@angular/core';
  import { RouterOutlet } from '@angular/router';

  @Component({
    selector: 'app-root',
    standalone: true,
    imports: [CommonModule, RouterOutlet],
    templateUrl: './app.component.html',
    styleUrl: './app.component.css',
  })
  export class AppComponent {
-  title = 'ng17-ssr'
+  private readonly http = inject(HttpClient);
+  uuid = '';
+  
+  ngOnInit(): void {
+    this.http
+      .get('https://www.uuidtools.com/api/generate/v4/count/1')
+      .subscribe((uuid: any) => {
+        this.uuid = uuid;
+      });
+    }
  }
```

```diff_html:app.component.html
+ UUID: {{ uuid }}
```

次に、ビルドします。

```zsh
$ npm run build
```

`dist/ng17-ssr/browser/index.html`にビルドされた結果が格納されます。

```html:index.html
// ...
<app-root _nghost-ng-c1557180121 ng-version="17.0.6" ngh="0" ng-server-context="ssg">UUID: 3a67fab7-ec86-418b-8b05-96f561a17457
</app-root>
```

API経由で取得したUUIDが埋め込まれていることが確認できました。


# 特定のパスだけプリレンダリングする

デフォルトでは全てのパスをプリレンダリングしますが、特定のパスのみを対象にすることもできます。
その場合、まず`angular.json`の設定を変更します。

```diff_json:angular.json
  {
-   "prerender": true
+   "prerender": {
+     "discoverRoutes": false,
+     "routesFile": "prerender-routes.txt"
+   }
  }
```

さらに、ルート直下に`prerender-routes.txt`を作成します。
例えば、`/foo`と`/bar`があって、`/bar`のみをSSGしたい場合は次のように記述します。

```txt:prerender-routes.txt
/bar
```

この状態でビルドすると、`dist/ng17-ssr/browser/bar/index.html`は生成されますが、`dist/ng17-ssr/browser/foo/index.html`は生成されないことが確認できます。


# デプロイする

Vercelへデプロイして動作確認してみます。
個人利用の範囲(Hobby Plan)であれば、無料で使用できます。

まず、Vercel用の設定を追加します。
ルート直下に`api/index.mjs`と`vercel.json`を追加します。
`ng17-ssr`の部分は各々のプロジェクト名に読み替えてください。

```javascript:index.mjs
import { app } from "../dist/ng17-ssr/server/server.mjs";

export default app();
```

```json:vercel.json
{
  "rewrites": [{ "source": "/(.*)", "destination": "/api" }],
  "functions": {
    "api/index.mjs": {
      "includeFiles": "dist/ng17-ssr/server/**"
    }
  }
}
```

GitHubにPushして、Vercelからリポジトリを選択、デプロイします。
デプロイ設定はFramework PresetにAngularを選択するだけでOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187787/a1f3aa84-e625-7ea0-72c9-f809b008999e.png)

無事にデプロイされたら動作確認してみましょう。

![ng17-ssr.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187787/f3cc3af3-af38-a697-f494-2e2498f98db5.gif)

`/foo`はプリレンダリングされていないので、リロードするたびに表示されるUUIDが変更されますが、`/bar`はプリレンダリングされているため、リロードしても同じUUIDが表示されます。


# まとめ

やってみると案外簡単でした。SSR/SSGのフレームワークとしてAngularを使うケースが、今後増えてくるといいですね。

既存のアプリケーションに対しても、SSRはサーバーが必要なので少しハードルが高いですが、プリレンダリングだけであれば、すぐにでも導入できるので、試してみてはいかがでしょうか。

明日は、[@komura_c](https://qiita.com/komura_c)さんです。


# 参考

https://angular.dev/guide/ssr

https://www.youtube.com/watch?v=Vx0hfGixkLA