← Cloudflare Workers / workers / examples
Shell Single Page App (SPA) s bootstrap daty
Tento příklad používá Worker a HTMLRewriter k vložení předem načtených dat API do shellu jednostránkové aplikace (SPA). Worker načítá bootstrapovací data paralelně s HTML shellem a výsledek streamuje do prohlížeče, takže SPA má vše potřebné ještě předtím, než se spustí její JavaScript.
Jsou zobrazeny dvě varianty:
- Static Assets : SPA je nasazena pomocí Workers Static Assets
- Externí origin : SPA je hostována mimo Cloudflare a Worker před ní stojí jako reverzní proxy, což zlepšuje výkon
Obě varianty používají stejnou techniku vkládání přes HTMLRewriter a stejný vzor zpracování na straně klienta. Vyberte tu, která odpovídá vašemu nasazení.
Tento vzor funguje s libovolným SPA frameworkem, například React, Vue, Svelte nebo jinými. Průvodce nasazením pro konkrétní framework najdete v Webové aplikace.
Možnost 1: Jednostránková aplikace (SPA) postavená celá na Workers
Tuto variantu použijte, pokud je výstup sestavení SPA nasazený jako součást Workeru pomocí Static Assets.
Nakonfigurujte statická aktiva
Nastavte not_found_handling na "single-page-application" tak, aby každá trasa vracela index.html. Použijte run_worker_first pro směrování všech požadavků přes váš Worker s výjimkou hashovaných assets pod /assets/*, které jsou obsluhovány přímo.
{
"name": "my-spa",
"main": "src/worker.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": "./dist",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"run_worker_first": ["/*", "!/assets/*"],
},
}name = "my-spa"
main = "src/worker.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[assets]
directory = "./dist"
binding = "ASSETS"
not_found_handling = "single-page-application"
run_worker_first = [ "/*", "!/assets/*" ]Podrobnosti o těchto možnostech najdete v Static Assets routing a run_worker_first reference.
Vkládání bootstrap dat pomocí HTMLRewriter
Worker začne okamžitě načítat data z API a poté načte SPA shell ze statických assetů. HTMLRewriter streamuje <head> prohlížeči ihned. Když <body> handler spustí, čeká na odpověď API a na začátek přidá <script> tag obsahující serializovaná data.
Pokud volání API selže, shell se přesto načte a SPA se přepne na načítání dat na straně klienta.
// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Serve root-level static files (favicon.ico, robots.txt) directly.
// Hashed assets under /assets/* skip the Worker entirely via run_worker_first.
if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
return env.ASSETS.fetch(request);
}
// Start fetching bootstrap data immediately — do not await yet.
const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);
// Fetch the SPA shell from static assets (co-located, sub-millisecond).
const shell = await env.ASSETS.fetch(
new Request(new URL("/index.html", request.url)),
);
// Use HTMLRewriter to stream the shell and inject data into <body>.
return new HTMLRewriter()
.on("body", {
async element(el) {
const data = await dataPromise;
if (data) {
el.prepend(
`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
{ html: true },
);
}
},
})
.transform(shell);
},
};
async function fetchBootstrapData(env, pathname, headers) {
try {
const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
headers: {
Cookie: headers.get("Cookie") || "",
"X-Request-Path": pathname,
},
});
if (!res.ok) return null;
return await res.json();
} catch {
// If the API is down, the shell still loads and the SPA
// falls back to client-side data fetching.
return null;
}
}// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Serve root-level static files (favicon.ico, robots.txt) directly.
// Hashed assets under /assets/* skip the Worker entirely via run_worker_first.
if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
return env.ASSETS.fetch(request);
}
// Start fetching bootstrap data immediately — do not await yet.
const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);
// Fetch the SPA shell from static assets (co-located, sub-millisecond).
const shell = await env.ASSETS.fetch(
new Request(new URL("/index.html", request.url)),
);
// Use HTMLRewriter to stream the shell and inject data into <body>.
return new HTMLRewriter()
.on("body", {
async element(el) {
const data = await dataPromise;
if (data) {
el.prepend(
`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
{ html: true },
);
}
},
})
.transform(shell);
},
} satisfies ExportedHandler<Env>;
async function fetchBootstrapData(
env: Env,
pathname: string,
headers: Headers,
): Promise<unknown | null> {
try {
const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
headers: {
Cookie: headers.get("Cookie") || "",
"X-Request-Path": pathname,
},
});
if (!res.ok) return null;
return await res.json();
} catch {
// If the API is down, the shell still loads and the SPA
// falls back to client-side data fetching.
return null;
}
}Možnost 2: SPA hostovaná na externím originu
Tuto variantu použijte, pokud jsou váš HTML, CSS a JavaScript nasazené mimo Cloudflare. Worker načte shell SPA z externího originu, pomocí HTMLRewriter do něj vloží bootstrap data a upravenou odpověď streamuje do prohlížeče.
Nakonfigurujte Worker
Protože SPA není součástí Workers Static Assets, nepotřebujete assets blok. Místo toho uložte externí URL adresu origin serveru jako proměnnou prostředí. Připojte Worker ke své doméně pomocí Custom Domain nebo Trasa.
{
"name": "my-spa-proxy",
"main": "src/worker.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"vars": {
"SPA_ORIGIN": "https://my-spa.example-hosting.com",
"API_BASE_URL": "https://api.example.com",
},
}name = "my-spa-proxy"
main = "src/worker.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[vars]
SPA_ORIGIN = "https://my-spa.example-hosting.com"
API_BASE_URL = "https://api.example.com"Vkládání bootstrap dat pomocí HTMLRewriter
Worker paralelně načítá SPA shell i data z API. Jakmile SPA origin odpoví, HTMLRewriter streamuje HTML a zároveň do něj vkládá bootstrap data do <body>. Statické assety (CSS, JS, obrázky) jsou předávány na externí origin beze změny.
// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Pass static asset requests through to the external origin unmodified.
if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
return fetch(new Request(`${env.SPA_ORIGIN}${url.pathname}`, request));
}
// Start fetching bootstrap data immediately — do not await yet.
const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);
// Fetch the SPA shell from the external origin.
// SPA routers serve index.html for all routes.
const shell = await fetch(`${env.SPA_ORIGIN}/index.html`);
if (!shell.ok) {
return new Response("Origin returned an error", { status: 502 });
}
// Use HTMLRewriter to stream the shell and inject data into <body>.
return new HTMLRewriter()
.on("body", {
async element(el) {
const data = await dataPromise;
if (data) {
el.prepend(
`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
{ html: true },
);
}
},
})
.transform(shell);
},
};
async function fetchBootstrapData(env, pathname, headers) {
try {
const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
headers: {
Cookie: headers.get("Cookie") || "",
"X-Request-Path": pathname,
},
});
if (!res.ok) return null;
return await res.json();
} catch {
// If the API is down, the shell still loads and the SPA
// falls back to client-side data fetching.
return null;
}
}// Env is generated by `wrangler types` — run it whenever you change your config.
// Do not manually define Env — it drifts from your actual bindings.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Pass static asset requests through to the external origin unmodified.
if (url.pathname.match(/\.\w+$/) && !url.pathname.endsWith(".html")) {
return fetch(new Request(`${env.SPA_ORIGIN}${url.pathname}`, request));
}
// Start fetching bootstrap data immediately — do not await yet.
const dataPromise = fetchBootstrapData(env, url.pathname, request.headers);
// Fetch the SPA shell from the external origin.
// SPA routers serve index.html for all routes.
const shell = await fetch(`${env.SPA_ORIGIN}/index.html`);
if (!shell.ok) {
return new Response("Origin returned an error", { status: 502 });
}
// Use HTMLRewriter to stream the shell and inject data into <body>.
return new HTMLRewriter()
.on("body", {
async element(el) {
const data = await dataPromise;
if (data) {
el.prepend(
`<script>window.__BOOTSTRAP_DATA__=${JSON.stringify(data)}</script>`,
{ html: true },
);
}
},
})
.transform(shell);
},
} satisfies ExportedHandler<Env>;
async function fetchBootstrapData(
env: Env,
pathname: string,
headers: Headers,
): Promise<unknown | null> {
try {
const res = await fetch(`${env.API_BASE_URL}/api/bootstrap`, {
headers: {
Cookie: headers.get("Cookie") || "",
"X-Request-Path": pathname,
},
});
if (!res.ok) return null;
return await res.json();
} catch {
// If the API is down, the shell still loads and the SPA
// falls back to client-side data fetching.
return null;
}
}Využijte přednačtená data ve vaší SPA
Na klientovi načtěte window.__BOOTSTRAP_DATA__ před jakýmkoli voláním API. Pokud data existují, použijte je přímo. V opačném případě proveďte běžný fetch.
// React example — works the same way in Vue, Svelte, or any other framework.
import { useEffect, useState } from "react";
function App() {
const [data, setData] = useState(window.__BOOTSTRAP_DATA__ || null);
const [loading, setLoading] = useState(!data);
useEffect(() => {
if (data) return; // Already have prefetched data — skip the API call.
fetch("/api/bootstrap")
.then((res) => res.json())
.then((result) => {
setData(result);
setLoading(false);
});
}, []);
if (loading) return <LoadingSpinner />;
return <Dashboard data={data} />;
}Přidejte deklaraci typu, aby TypeScript rozpoznal globální vlastnost:
declare global {
interface Window {
__BOOTSTRAP_DATA__?: unknown;
}
}Další techniky injektáže
Můžete zřetězit více handlerů HTMLRewriter a vkládat tak více než jen bootstrap data.
Nastavit meta tagy
Vložte Open Graph nebo jiné <meta> tagy na základě cesty požadavku. Díky tomu dostávají prohledávače sociálních sítí správné náhledy bez nutnosti plnohodnotného frameworku pro server-side rendering.
new HTMLRewriter()
.on("head", {
element(el) {
el.append(`<meta property="og:title" content="${title}" />`, {
html: true,
});
},
})
.transform(shell);Přidání CSP nonce
Vygenerujte nonce pro každý požadavek a vložte ji do hlavičky Content-Security-Policy i do každého vloženého <script> .
const nonce = crypto.randomUUID();
const response = new HTMLRewriter()
.on("script", {
element(el) {
el.setAttribute("nonce", nonce);
},
})
.transform(shell);
response.headers.set(
"Content-Security-Policy",
`script-src 'nonce-${nonce}' 'strict-dynamic';`,
);
return response;Vložte uživatelskou konfiguraci
Zpřístupněte feature flags nebo nastavení specifická pro dané prostředí aplikaci SPA bez potřeby dalšího volání API.
new HTMLRewriter()
.on("body", {
element(el) {
el.prepend(
`<script>window.__APP_CONFIG__=${JSON.stringify({
apiBase: env.API_BASE_URL,
featureFlags: { darkMode: true },
})}</script>`,
{ html: true },
);
},
})
.transform(shell);Související zdroje
- HTMLRewriter : streamovací HTML parser a transformer.
- Workers Static Assets : obsluhujte statické soubory společně se svým Workerem.
- Static Assets routing : nakonfigurujte
run_worker_firstanot_found_handling. - Static Assets binding : reference pro
ASSETSbinding a možnosti směrování. - Custom Domains : připojte Worker k doméně jako origin.
- Trasy : spusťte Worker před existujícím origin serverem.
- Osvědčené postupy pro Workers : vzory kódu a doporučení pro konfiguraci Workers.