あ〜CesiumJSをSSR環境でも動かしたいね〜

:::note info
正確に言えば、あ〜SSR環境で動作するアプリケーション内でもCesiumJSを利用したいね〜です。
:::

```
使用環境
Svelte v5.0.0
SvelteKit v2.0.0
pnpm v9.14.4
```

# TL;DR
ビルド時に`vite-plugin-static-copy`を用いて、クライアントサイドで使用するパッケージを静的ファイルにコピーする。
試してみたいくらいだったらCDN使えば良い。

# はじめに
CesiumJSはロード時にパッケージ内の静的ファイル（AssetsやWorkersなど）をクライアントサイド（ブラウザ）で動的に読み込む必要がある。このため、CesiumJS自体はサーバーサイドレンダリング（SSR）には適さず、多くの場合アプリケーション全体もSSG（静的サイト生成）またはCSR（クライアントサイドレンダリング）環境で動作させることが一般的となる。
これは、CesiumJSがWebGLベースで動作し、大量の静的リソースを効率よく配信する設計だからとも言える。

一方で現代のウェブ開発要件に応じてSSRやハイブリッドレンダリングへの移行も検討されるようになった。例えばAWS Amplifyなどのモダンなホスティング環境では、Lambda@Edgeを活用したSSRによる初期表示速度向上とエッジコンピューティングによる効率的な配信が容易に実現可能となる。このような仕組みを活用することで、CesiumJSの3D描画機能と最新ウェブ技術の利点を両立することができる。

この記事では、アプリケーション全体をSSR環境で動作させつつ、クライアントサイドでのロード・動作する必要があるCesiumJSとの適合方法について解説する。

# やらないこと
- AWS Amplifyへの詳細なデプロイ手順

# 準備
Svelte5のプロジェクトを作成し、ローカル環境でCesiumJSが動作するところまで

## プロジェクト作成

```
npx sv create svelte5-cesium
```
選択したオプション
```
┌  Welcome to the Svelte CLI! (v0.6.11)
│
◇  Which template would you like?
│  SvelteKit minimal
│
◇  Add type checking with Typescript?
│  Yes, using Typescript syntax
│
◆  Project created
│
◇  What would you like to add to your project? (use arrow keys / space bar)
│  tailwindcss
│
◇  Which plugins would you like to add?
│  none
│
◇  Which package manager do you want to install dependencies with?
│  pnpm
```
パッケージマネージャにはpnpmを採用、tailwindcssも入れた。

`pnpm run dev`で起動すると↓が表示できる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/07313a4b-5661-3150-56de-40a20f59d9b1.png)

## CesiumJSの導入
`src/routes/+page.svelte`に以下を記載する。

```
<script lang="ts">
  import { onMount } from "svelte";
  import { UrlTemplateImageryProvider, Viewer, Cartesian3 } from "cesium";
  import("cesium/Build/Cesium/Widgets/widgets.css");

  // CesiumJSの静的リソース（AssetsやWorkers）をロードするためのベースURL
  const baseUrl = "/node_modules/cesium/Build/Cesium/";

  onMount(() => {
    // CesiumJSの静的リソースのベースURLをグローバル変数に設定
    (window as any).CESIUM_BASE_URL = baseUrl;

    // Cesium Viewerインスタンスの初期化（地図コンテナを指定）
    const viewer = new Viewer("cesiumContainer", {});

    // 地理院地図の衛星画像プロバイダー
    const gsiSeamless = new UrlTemplateImageryProvider({
      url: "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg",
    });

    // 作成した画像プロバイダーをCesium Viewerに追加
    viewer.imageryLayers.addImageryProvider(gsiSeamless);

    // カメラの初期位置
    viewer.camera.setView({
      destination: Cartesian3.fromDegrees(139.767052, 35.681167, 10000),
    });
  });
</script>

<div id="cesiumContainer" class="cesiumContainer h-screen w-screen"></div>
```

すると画面いっぱいの地図画面が表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/a6725fef-0ef3-ee2e-70dc-a88715571bff.png)

地形は適用していないのでのっぺら

# AWS Amplifyにホスティング
AWS Amplify用にビルドしてとりあえずホスティングしてみる。

## ビルド設定
AWS Amplify用にビルドする必要があるので、`svelte.config.js` にてadapterを`amplify-adapter`に変更する。
```
import amplifyAdapter from 'amplify-adapter';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	preprocess: vitePreprocess(),

	kit: {
		adapter: amplifyAdapter()
	}
};

export default config;
```

`pnpm run dev`でビルドするとPJルートの`build`配下にビルドファイルが生成されているはず（output先を他に設定していたら別）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/53ab873f-057a-a5dc-de70-aaf284acebe4.png)

## デプロイ
GitHubにリポジトリを作成してデプロイしてみる。

リポジトリを作成後、AWS Amplifyのマネコンからデプロイの設定を行う。

リポジトリ/ブランチを選択後の「アプリケーションの設定」にて
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/deb03a29-a45c-d6cf-0256-b3f3c0ae034f.png)
出力ディレクトリをビルドにはビルド設定でoutputが出力された`build`ディレクトリを指定する。

当然ビルドに失敗する。

pnpmがないので、`pnpm install`を実行する前にインストールする必要がある。

最終的なコード
```
version: 1
frontend:
    phases:
        preBuild:
            commands:
                - npm install -g pnpm@9.14.4
                - pnpm install
        build:
            commands:
                - pnpm run build
    artifacts:
        baseDirectory: build
        files:
            - '**/*'
    cache:
        paths:
            - 'node_modules/**/*'
```
- Cesium ionを使用するため環境変数にトークンを埋め込みたい場合は、ホスティング > 環境変数に設定後、ビルド設定にでも含めてあげる必要がある
- キャッシュのパスに`.pnpm-store/**/*`を含めることで、pnpmらしい更なるビルド時間短縮と安定性向上が期待できる

## デプロイ結果
デプロイ済みのドメインに接続してみると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/02e6ee5b-234f-c1a3-2963-88a2a453f257.png)
エラーが出ている。

コンソールにはCeisumJSのライブラリに含まれている静的ファイルが取得できなかった旨のエラーが表示されている。
![スクリーンショット 2025-01-16 4.36.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/12caabd9-3020-6799-8dd0-7fbcf995b104.png)


# 対処法
## その1
上記のエラーはクライアントサイドでCeiumJSが静的ファイルを読み込もうとしたものの、該当のパッケージの静的ファイルがクライアントサイドに存在しないことに起因している。
そこでビルド時に`vite-plugin-static-copy`を用いて、クライアントサイドで使用するパッケージを静的ファイルにコピーする。
https://www.npmjs.com/package/vite-plugin-static-copy

`vite.config.ts`にて以下のように設定することで、ビルドファイル`static`に指定したファイル（今回はcesiumパッケージの各ファイル）を含めることができる。

```
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';

export default defineConfig({
	plugins: [
		sveltekit(),
		viteStaticCopy({
			targets: [
				{
					src: 'node_modules/cesium/Build/Cesium/Workers/**/*',
					dest: 'Cesium/Workers'
				},
				{
					src: 'node_modules/cesium/Build/Cesium/Assets/**/*',
					dest: 'Cesium/Assets'
				},
				{
					src: 'node_modules/cesium/Build/Cesium/Widgets/**/*',
					dest: 'Cesium/Widgets'
				}
			]
		})
	],
});
```

`pnpm run build`した結果、`build > static`配下に`Cesium`が存在することがわかる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/fa127a24-4cd7-e410-b2db-dda7b44d9fba.png)

クライアントサイドの静的ファイルにCesiumJSのパッケージが含まれても、`CESIUM_BASE_URL`がそのままだと参照できないことに変わりはない。

そこで環境ごとに`CESIUM_BASE_URL`を切り替える必要がある。
```
// CesiumJSの静的リソース（AssetsやWorkers）をロードするためのベースURL
const baseUrl = import.meta.env.DEV
    ? "/node_modules/cesium/Build/Cesium/"
    : "/Cesium/";
```
ここでは`vite-plugin-static-copy`で指定したビルドファイルに含めるディレクトリ名を指定する。

すると開発環境ではインストールした`node_module`を、検証・本番環境ではビルド時に含められていたパッケージを使用することができる。

デプロイすると無事見られるようになる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3336887/04f8a3d0-58a9-0331-8141-0ed3d6aec763.png)

## その2

`CESIUM_BASE_URL`にCDN URLを指定する。

```
const baseUrl = "https://cdn.jsdelivr.net/npm/cesium@1.125.0/Build/Cesium/";
```

# まとめ
SSR環境で動作するアプリケーションにおいて、クライアントサイドでCesiumJSのパッケージを読み込むとき、CDNを直で読み込めば終わりだが以下の観点から、私は`vite-plugin-static-copy`の使用を推したい。
1. 開発環境と検証・本番環境でのCesiumJS動作環境のバージョン差異が防げる
2. CDN依存性によるリスク（プロバイダー障害やキャッシュ問題）への対策

一方で、CDNの特性上パフォーマンスの観点やビルド成果物の軽量化においてはCDNのメリットは挙げられる。
ちょっと検証するくらいだったら正直CDNでいいかなとも思う。