跳转到内容
搜索文档

单页应用(SPA)

最后更新 查看 MarkdownAgent 设置

单页应用(SPA)是在客户端渲染(CSR)的 Web 应用。它们通常使用 ReactVueSvelte 等框架构建。这些框架的构建过程会生成单个 /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-08-17",
	"assets": {
		"directory": "./dist/",
		"not_found_handling": "single-page-application"
	}
}
name = "my-worker"
# Set this to today's date
compatibility_date = "2026-08-17"

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

assets.not_found_handling 配置为 single-page-application 会覆盖 Workers 静态资源的默认服务行为。当传入请求未匹配 assets.directory 中的文件时,Workers 会以 200 OK 状态提供 /index.html 文件的内容。

导航请求

如果你有一个 Worker 脚本(main),已配置 assets.not_found_handling,并使用 assets_navigation_prefers_asset_serving 兼容性标志(或设置兼容性日期为 2025-04-01 或更高版本),导航请求 将不会调用 Worker 脚本。导航请求 是使用 Sec-Fetch-Mode: navigate 标头发出的请求,浏览器在导航到页面时会自动附加此标头。这减少了 Worker 脚本的可计费调用次数,对于客户端密集型应用程序特别有用,否则这些应用程序会非常频繁且不必要地调用 Worker 脚本。

客户端回调

在某些情况下,你可能需要将导航请求中的值传递给 Worker 脚本。例如,如果你充当 OAuth 回调,你可能会看到向 /oauth/callback?code=... 等路由发出的请求。使用 assets_navigation_prefers_asset_serving 标志时,将提供 HTML 资源,而不是 Worker 脚本。在这种情况下,我们建议你通过客户端 JavaScript 将值传递给服务器,可以在此适当路由的客户端应用程序中完成,或使用精简的端点特定 HTML 文件。

./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-08-17",
	"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-17"
main = "./src/index.ts"

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

此配置提供明确的路由控制,无需依赖浏览器导航请求头,非常适合需要精细路由行为的复杂 SPA。Worker 脚本随后可以处理匹配的路由(可选地使用 assets 绑定(binding))并提供动态内容。

示例:

./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 shell 到达浏览器之前注入数据。有关使用 HTMLRewriter 预取 API 数据并嵌入 HTML 流的完整示例,请参阅 带引导数据的 SPA shell

本地开发

如果你使用的是基于 Vite 的 SPA 框架,可以考虑使用我们的 Vite 插件,它提供原生 Vite 开发体验。

参考

在大多数情况下,将 assets.not_found_handling 配置为 single-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-.->|Assets 绑定|AssetServing
RequestNavigation-->|是|AssetServing

subgraph Asset serving
	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 脚本时才计费。从那里,可以使用 assets 绑定提供资源(如上图中的虚线所示)。

虽然不太可能影响 SPA 的提供方式,但你可以在 HTML 处理文档中阅读有关我们如何匹配资源的更多信息。

这篇文档对您有帮助吗?