Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

シングルページアプリケーション(SPA)

最終更新 Markdown で表示Agent セットアップ

シングルページアプリケーション(SPA)は、クライアントサイドレンダリング(CSR)の Web アプリケーションです。ReactVueSvelte などのフレームワークで構築することがよくあります。これらのフレームワークのビルドでは、1 つの /index.html ファイルと、それに付随するクライアントサイドのリソース(JavaScript バンドル、CSS スタイルシート、画像、フォントなど)が生成されます。データは通常、クライアントが API へクライアントサイドリクエストを送って取得します。

single-page-application モードを設定すると、Cloudflare はデフォルトのルーティング動作を提供します。ほかのアセットに一致しないナビゲーションリクエスト(Sec-Fetch-Mode: navigate ヘッダー付き)に対して、/index.html ファイルを自動で配信します。どのパスで Worker スクリプトを呼び出すかをより細かく制御するには、高度なルーティング制御 を使えます。

設定

シングルページアプリケーションを Workers にデプロイするには、Wrangler 設定ファイルassets.directoryassets.not_found_handling オプションを設定します。

{
	"name": "my-worker",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"assets": {
		"directory": "./dist/",
		"not_found_handling": "single-page-application"
	}
}
name = "my-worker"
# Set this to today's date
compatibility_date = "2026-09-20"

[assets]
directory = "./dist/"
not_found_handling = "single-page-application"

assets.not_found_handlingsingle-page-application に設定すると、静的アセット向けの Workers のデフォルト配信動作が上書きされます。受信リクエストが assets.directory 内のファイルに一致しない場合、Workers は /index.html の内容を 200 OK ステータスで配信します。

ナビゲーションリクエスト

Worker スクリプト(main)があり、assets.not_found_handling を設定しており、かつ assets_navigation_prefers_asset_serving compatibility flag を使っている(または compatibility date を 2025-04-01 以降にしている)場合、ナビゲーションリクエスト は Worker スクリプトを呼び出しません。ナビゲーションリクエスト は、Sec-Fetch-Mode: navigate ヘッダー付きのリクエストです。ブラウザーはページへ遷移するときに、このヘッダーを自動で付けます。これにより Worker スクリプトの課金対象の呼び出しが減ります。クライアント側が中心のアプリケーションでは、そうしなければ Worker スクリプトが不要なほど頻繁に呼び出されるため、特に有効です。

クライアント側のコールバック

ナビゲーションリクエストから Worker スクリプトへ値を渡す必要がある場合があります。たとえば OAuth コールバックとして動作させるとき、/oauth/callback?code=... のようなルートへのリクエストを想定することがあります。assets_navigation_prefers_asset_serving フラグがある場合、Worker スクリプトではなく HTML アセットが配信されます。この場合は、該当ルート向けのクライアントアプリケーション内、またはエンドポイント専用の簡易 HTML ファイルで、クライアント側の JavaScript からサーバーへ値を渡すことを推奨します。

./dist/oauth/callback.htmlhtml
<!DOCTYPE html>
<html>
	<head>
		<title>OAuth callback</title>
	</head>
	<body>
		<p>Loading...</p>
		<script>
			(async () => {
				const response = await fetch("/api/oauth/callback" + window.location.search);
				if (response.ok) {
					window.location.href = '/';
				} else {
					document.querySelector('p').textContent = 'Error: ' + (await response.json()).error;
				}
			})();
		</script>
	</body>
</html>
./worker/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

export default class extends WorkerEntrypoint {
	async fetch(request) {
		const url = new URL(request.url);
		if (url.pathname === "/api/oauth/callback") {
			const code = url.searchParams.get("code");

			const sessionId =
				await exchangeAuthorizationCodeForAccessAndRefreshTokensAndPersistToDatabaseAndGetSessionId(
					code,
				);

			if (sessionId) {
				return new Response(null, {
					headers: {
						"Set-Cookie": `sessionId=${sessionId}; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=86400`,
					},
				});
			} else {
				return Response.json(
					{ error: "Invalid OAuth code. Please try again." },
					{ status: 400 },
				);
			}
		}

		return new Response(null, { status: 404 });
	}
}
./worker/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";

export default class extends WorkerEntrypoint {
	async fetch(request: Request) {
		const url = new URL(request.url);
		if (url.pathname === "/api/oauth/callback") {
			const code = url.searchParams.get("code");

			const sessionId = await exchangeAuthorizationCodeForAccessAndRefreshTokensAndPersistToDatabaseAndGetSessionId(code);

			if (sessionId) {
				return new Response(null, {
					headers: {
						"Set-Cookie": `sessionId=${sessionId}; HttpOnly; SameSite=Strict; Secure; Path=/; Max-Age=86400`,
					},
				});
			} else {
				return Response.json(
					{ error: "Invalid OAuth code. Please try again." },
					{ status: 400 }
				);
			}
		}

		return new Response(null, { status: 404 });
	}
}

高度なルーティング制御

SPA のルーティング動作をより明示的に制御するには、ルートパターンの配列を指定した run_worker_first を使います。この方法では、Sec-Fetch-Mode: navigate の自動検出が無効になり、どのリクエストを Worker スクリプトで処理し、どれを静的アセットとして配信するかを明示的に決められます。

{
	"name": "my-worker",
	// Set this to today's date
	"compatibility_date": "2026-09-20",
	"main": "./src/index.ts",
	"assets": {
		"directory": "./dist/",
		"not_found_handling": "single-page-application",
		"binding": "ASSETS",
		"run_worker_first": ["/api/*", "!/api/docs/*"]
	}
}
name = "my-worker"
# Set this to today's date
compatibility_date = "2026-09-20"
main = "./src/index.ts"

[assets]
directory = "./dist/"
not_found_handling = "single-page-application"
binding = "ASSETS"
run_worker_first = [ "/api/*", "!/api/docs/*" ]

この設定では、ブラウザーのナビゲーションヘッダーに依存せず、ルーティングを明示的に制御できます。細かいルーティングが必要な複雑な SPA に適しています。Worker スクリプトは一致したルートを処理し、(任意で アセットバインディング を使い)動的コンテンツを配信できます。

例:

./src/index.jsjs
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (url.pathname === "/api/name") {
			return new Response(JSON.stringify({ name: "Cloudflare" }), {
				headers: { "Content-Type": "application/json" },
			});
		}

		return new Response(null, { status: 404 });
	},
};
./src/index.tsts
export default {
	async fetch(request, env): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname === "/api/name") {
			return new Response(JSON.stringify({ name: "Cloudflare" }), {
				headers: { "Content-Type": "application/json" },
			});
		}

		return new Response(null, { status: 404 });
	},
} satisfies ExportedHandler;

run_worker_first を使い、SPA シェルがブラウザーに届く前にデータを埋め込むこともできます。HTMLRewriter で API データを先読みし、HTML ストリームに埋め込む完全な例は ブートストラップデータを持つ SPA シェル を参照してください。

ローカル開発

Vite ベースの SPA フレームワークを使っている場合は、Vite ネイティブな開発体験を提供する Vite plugin が役立つことがあります。

リファレンス

多くの場合、assets.not_found_handlingsingle-page-application に設定すれば、期待する動作になります。独自フレームワークを作っている場合や、特別な要件がある場合は、次の図でルーティング判定の詳細を確認できます。

ルーティング判定の全体図
flowchart
  Request@{ shape: stadium, label: "受信リクエスト" }
  Request-->RunWorkerFirst
  RunWorkerFirst@{ shape: diamond, label: "Worker スクリプトを先に実行する?" }
  RunWorkerFirst-->|リクエストが run_worker_first のパスに一致|WorkerScriptInvoked
  RunWorkerFirst-->|リクエストが run_worker_first の否定パスに一致|AssetServing
  RunWorkerFirst-->|一致なし|RequestMatchesAsset
  RequestMatchesAsset@{ shape: diamond, label: "リクエストがアセットに一致する?" }
  RequestMatchesAsset-->|はい|AssetServing
  RequestMatchesAsset-->|いいえ|WorkerScriptPresent
  WorkerScriptPresent@{ shape: diamond, label: "Worker スクリプトがある?" }
  WorkerScriptPresent-->|いいえ|AssetServing
  WorkerScriptPresent-->|はい|RequestNavigation
  RequestNavigation@{ shape: diamond, label: "ナビゲーションリクエスト?" }
  RequestNavigation-->|いいえ|WorkerScriptInvoked
  WorkerScriptInvoked@{ shape: rect, label: "Worker スクリプトを呼び出し" }
  WorkerScriptInvoked-.->|アセットバインディング|AssetServing
  RequestNavigation-->|はい|AssetServing

  subgraph assetServing ["アセット配信"]
  	AssetServing@{ shape: diamond, label: "リクエストがアセットに一致する?" }
  	AssetServing-->|はい|AssetServed
  	AssetServed@{ shape: stadium, label: "**200 OK**<br />アセットを配信" }
  	AssetServing-->|いいえ|NotFoundHandling

  	subgraph single-page-application
  		NotFoundHandling@{ shape: rect, label: "リクエストを /index.html に書き換え" }
  		NotFoundHandling-->SPAExists
  		SPAExists@{ shape: diamond, label: "HTML ページがある?" }
  		SPAExists-->|はい|SPAServed
  		SPAExists-->|いいえ|Generic404PageServed
  		Generic404PageServed@{ shape: stadium, label: "**404 Not Found**<br />空の本文レスポンスを配信" }
  		SPAServed@{ shape: stadium, label: "**200 OK**<br />/index.html ページを配信" }
  	end

  end

課金対象になるのは、Worker スクリプトが呼び出された場合だけです。そのあと、アセットバインディングを使ってアセットを配信できます(上の図の点線)。

SPA の配信に影響することは少ないですが、アセットの一致方法の詳細は HTML 処理のドキュメント を参照してください。

役に立ちましたか?