单页应用(SPA)是在客户端渲染(CSR)的 Web 应用。它们通常使用 React、Vue 或 Svelte 等框架构建。这些框架的构建过程会生成单个 /index.html 文件以及配套的客户端资源(例如 JavaScript 包、CSS 样式表、图片、字体等)。数据通常由客户端通过 API 发起请求获取。
当你配置 single-page-application 模式时,Cloudflare 会提供默认路由行为:对于导航请求(带有 Sec-Fetch-Mode: navigate 请求头)且未匹配任何其他静态资源时,自动提供 /index.html 文件。如需更精细地控制哪些路径会调用 Worker 脚本,请参阅高级路由控制。
要将单页应用部署到 Workers,必须在 Wrangler 配置文件 中配置 assets.directory 和 assets.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 文件。
<!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>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 });
}
}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))并提供动态内容。
示例:
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 });
},
};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 处理文档中阅读有关我们如何匹配资源的更多信息。