🅰️

Angular17でCSR/SSR/SSGを組み合わせる

2023/12/29に公開

はじめに

Angular17で Client Side Rendering (CSR) / Server Side Rendering (SSR) / Static Site Generation (SSG) を併用する方法をまとめます。

Angular で CSR / SSR / SSG の併用

まずはAngularのプロジェクトにSSRを導入します。

https://angular.dev/guide/ssr

$ ng new --ssr # 新プロジェクトを生成
$ ng add @angular/ssr # 既存プロジェクトにSSRを導入

この時点で以下のようなExpressのサーバが生成されます。
以下の通り全リクエストで dist 配下の server/index.server.html を返しています。

server.ts
// 〜(略)〜
export function app(): express.Express {
  const server = express();
  // serverDistFolder → /{PJ-PATH}/dist/{PJ-NAME}/server
  const serverDistFolder = dirname(fileURLToPath(import.meta.url));
  // browserDistFolder → /{PJ-PATH}/dist/{PJ-NAME}/browser
  const browserDistFolder = resolve(serverDistFolder, '../browser');
  // indexHtml → /{PJ-PATH}/dist/{PJ-NAME}/server/index.server.html
  const indexHtml = join(serverDistFolder, 'index.server.html');

  // 〜(略)〜

  // 全リクエストで indexHtml すなわち dist 配下の server/index.server.html を返却
  server.get('*', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;
    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });

  return server;
}
// 〜(略)〜

SSG の利用

SSG は angular.json に prerendering したいパスを指定するだけです。

https://angular.dev/guide/prerendering

以下のようなテキストファイルを用意し

prerender-routes.txt
/ssg

angular.jsonprerender にファイル名を記載します。

 "prerender": {
    "discoverRoutes": false,
    "routesFile": "prerender-routes.txt"
  },

npm run build すると browser/ssg/index.html というファイルができます。
以下のように専用のパスを作ってもアクセスできますが、従来のSSRのパスの commonEngine.render() 経由でも問題なく SSG されます。

server.ts
// 〜(略)〜
  server.get('/ssg', (req, res, next) => {
    res.sendFile(join(browserDistFolder, 'ssg/index.html'));
  });
// 〜(略)〜

commonEngine チョットヨクワカラナイ...

CSR / SSR の併用

CSR / SSR の併用は CSR したいパスへのリクエストに browser/index.html を返却 することで SSR と CSR が共存 できそうです。
以下のように server.tsbrowser/index.html を返却するパスを追加します。

server.ts
// 〜(略)〜
  server.get('/csr', (req, res, next) => {
    res.sendFile(join(browserDistFolder, 'index.html'));
  });

  server.get('*', (req, res, next) => {
    // 〜(略)〜
  });
// 〜(略)〜

http://{ SERVER }/csr にアクセスすると browser/index.html を取得し、ngOnInit などに書かれた fetch などの処理が走ることが確認できます。
http://{ SERVER }/ssr にアクセスすると、レンダリング済の server/index.server.html が表示されます。

CSR / SSR ページ間の遷移を確認

CSR と SSR 間のページ遷移を見てみましょう。
routerLink での遷移は <router-outlet> 内に表示するコンポーネントを差し替えます。

CSR 用の browser/index.html から SSR のパスへ routerLink で遷移するとbrowser/index.html のまま SSR 画面の表示を行うため、実際にはClient Side Renderingとなります。

初期表示 初期表示 遷移方法 遷移後 遷移後ページ
CSR browser/index.html routerLink SSR browser/index.html
CSR browser/index.html href SSR server/index.server.html
SSR server/index.server.html routerLink CSR server/index.server.html
SSR server/index.server.html href CSR browser/index.html

リクエストのキャッシュについて

SSG ページであっても ngOnInit に Fetch 処理が書かれていると、ブラウザに表示されてから再び Fetch 処理が走ってしまいます。
TransferStateisPlatformServer を利用することでサーバサイドのみで Fetch し、そのデータを保持することができます。

ssg.component.ts
// 〜(略)〜
export class SsgComponent {
  platformId = inject(PLATFORM_ID);
  transferState = inject(TransferState);
  stateKey = makeStateKey<Todo>('todo-state-key');

  td = signal<Todo>({});

  ngOnInit(): void {
    if(isPlatformServer(this.platformId)){
        this.todoService.findOneTodo().subscribe((res) => {
          this.td.set(res);
          this.transferState.set<Todo>(this.stateKey, res);
        }
      )
    }
    this.td.set(this.transferState.get<Todo>(this.stateKey, {}));
  }
}

また、Requestキャッシュについては別の記事を書きましたのでそちらをどうぞ。

https://zenn.dev/nao50/articles/angular17-interceptor

まとめ

Angular17で CSR / SSR / SSG の併用についてまとめました。
近年のAngularは standalone componentssignalsなど開発体験の向上が凄まじいですね。

Roadmapのzone.jsからの脱却など更なるパフォーマンス向上が期待できそうです。
個人的にRxjsがなくなると、より初心者に優しくなると感じているので期待をしています。

https://angular.dev/roadmap

GitHubで編集を提案

Discussion