INTEGRITY Документация

Одностраничное приложение (SPA)

Одностраничные приложения (SPA) представляют собой веб-приложения с клиентским рендерингом (CSR). Их часто создают с помощью таких фреймворков, как React, Vue или Svelte. Процесс сборки этих фреймворков создаст один /index.html файл и сопутствующие клиентские ресурсы (например, JavaScript-бандлы, CSS-стили, изображения, шрифты и т. д.). Обычно данные запрашиваются клиентом у API с помощью клиентских запросов.

Когда вы настраиваете single-page-application режиме Cloudflare обеспечивает маршрутизацию по умолчанию, которая автоматически обслуживает ваш /index.html файл для навигационных запросов (тех, у которых Sec-Fetch-Mode: navigate заголовки), которые не совпадают ни с одним другим ассетом. Чтобы точнее контролировать, какие пути вызывают ваш скрипт Worker, можно использовать расширенное управление маршрутизацией.

Конфигурация

Чтобы развернуть одностраничное приложение (SPA) в Workers, необходимо настроить assets.directory и assets.not_found_handling параметры в вашем конфигурационный файл Wrangler:

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

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

Настройка assets.not_found_handling к single-page-application переопределяет поведение обслуживания статических ресурсов Workers по умолчанию. Если входящий запрос не соответствует ни одному файлу в assets.directory, Workers будет обслуживать содержимое /index.html файл с 200 OK статус.

Если у вас есть скрипт Worker (main), настроили assets.not_found_handling, и используйте assets_navigation_prefers_asset_serving флаг совместимости (или установите дату совместимости 2025-04-01 или новее), запросы навигации не вызовет скрипт Worker. запрос навигации представляет собой запрос, выполненный с помощью Sec-Fetch-Mode: navigate заголовок, который браузеры автоматически прикрепляют при переходе на страницу. Это снижает число оплачиваемых вызовов скрипта Worker и особенно полезно для приложений с большой клиентской логикой, которые иначе вызывали бы скрипт Worker слишком часто и без необходимости.

Клиентские колбэки

В некоторых случаях может потребоваться передать значение из запроса навигации в скрипт Worker. Например, если вы выступаете в роли OAuth callback, вы можете ожидать запросы к такому маршруту, как /oauth/callback?code=.... С помощью assets_navigation_prefers_asset_serving флаг, ваши HTML-ресурсы будут отдаваться напрямую, а не через скрипт Worker. В этом случае мы рекомендуем передавать значение на сервер с помощью клиентского JavaScript, либо в составе вашего клиентского приложения для соответствующего маршрута, либо через отдельный облегчённый HTML-файл для конкретной конечной точки.

./dist/oauth/callback.html
<!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.js
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.ts
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-08-28",
	"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-08-28"
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.js
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.ts
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-оболочки с bootstrap-данными.

Локальная разработка

Если вы используете SPA-фреймворк на основе Vite, вам может быть интересно использовать Плагин Vite который предлагает разработку в нативной среде Vite.

Справочник

В большинстве случаев настройка assets.not_found_handling к single-page-application обеспечит нужное поведение. Если вы создаёте собственный фреймворк или у вас есть особые требования, следующая диаграмма поможет понять, как именно принимаются решения о маршрутизации.

Полная диаграмма принятия решений маршрутизации
flowchart
Request@{ shape: stadium, label: "Incoming request" }
Request-->RunWorkerFirst
RunWorkerFirst@{ shape: diamond, label: "Run Worker script first?" }
RunWorkerFirst-->|Request matches run_worker_first path|WorkerScriptInvoked
RunWorkerFirst-->|Request matches run_worker_first negative path|AssetServing
RunWorkerFirst-->|No matches|RequestMatchesAsset
RequestMatchesAsset@{ shape: diamond, label: "Request matches asset?" }
RequestMatchesAsset-->|Yes|AssetServing
RequestMatchesAsset-->|No|WorkerScriptPresent
WorkerScriptPresent@{ shape: diamond, label: "Worker script present?" }
WorkerScriptPresent-->|No|AssetServing
WorkerScriptPresent-->|Yes|RequestNavigation
RequestNavigation@{ shape: diamond, label: "Request is navigation request?" }
RequestNavigation-->|No|WorkerScriptInvoked
WorkerScriptInvoked@{ shape: rect, label: "Worker script invoked" }
WorkerScriptInvoked-.->|Asset binding|AssetServing
RequestNavigation-->|Yes|AssetServing

subgraph Asset serving
	AssetServing@{ shape: diamond, label: "Request matches asset?" }
	AssetServing-->|Yes|AssetServed
	AssetServed@{ shape: stadium, label: "**200 OK**<br />asset served" }
	AssetServing-->|No|NotFoundHandling

	subgraph single-page-application
		NotFoundHandling@{ shape: rect, label: "Request rewritten to /index.html" }
		NotFoundHandling-->SPAExists
		SPAExists@{ shape: diamond, label: "HTML Page exists?" }
		SPAExists-->|Yes|SPAServed
		SPAExists-->|No|Generic404PageServed
		Generic404PageServed@{ shape: stadium, label: "**404 Not Found**<br />null-body response served" }
		SPAServed@{ shape: stadium, label: "**200 OK**<br />/index.html page served" }
	end

end

Запросы тарифицируются только при вызове скрипта Worker. После этого можно обслуживать ресурсы с помощью привязки assets (на диаграмме выше показана пунктирной линией).

Хотя это вряд ли повлияет на то, как обслуживается SPA, подробнее о том, как мы сопоставляем ресурсы, можно прочитать в Документация по обработке HTML.