← Cloudflare Workers / workers / examples
Оболочка одностраничного приложения (SPA) с данными начальной загрузки
В этом примере используются Worker и HTMLRewriter чтобы внедрять заранее полученные данные API в оболочку одностраничного приложения (SPA). Worker параллельно загружает начальные данные и оболочку HTML и передает результат в браузер потоком, поэтому к моменту запуска JavaScript у SPA уже есть всё необходимое.
Показаны два варианта:
- Static Assets : SPA развёртывается с помощью Workers Static Assets
- Внешний источник : SPA размещено вне Cloudflare, а Worker стоит перед ним в роли обратного прокси, что повышает производительность
Оба варианта используют одну и ту же технику внедрения через HTMLRewriter и одинаковый способ использования на стороне клиента. Выберите вариант, который соответствует вашему развёртыванию.
Этот подход подходит для любого SPA фреймворка: React, Vue, Svelte и других. Инструкции по развертыванию для конкретных фреймворков см. Веб-приложения.
Вариант 1: одностраничное приложение (SPA), полностью построенное на Workers
Используйте этот вариант, если результат сборки SPA развёрнут как часть вашего Worker с помощью Static Assets.
Настройка статических ресурсов
Задайте not_found_handling к "single-page-application" чтобы каждый маршрут возвращал index.html. Используйте run_worker_first чтобы направлять все запросы через Worker, кроме хешированных ресурсов в /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/*"],
},
}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/*" ]Подробнее об этих параметрах см. в Маршрутизация Static Assets и run_worker_first справочник.
Внедрение bootstrap-данных с помощью HTMLRewriter
Worker сразу начинает загружать данные API, а затем загружает оболочку SPA из статических ресурсов. HTMLRewriter передает потоком <head> в браузер сразу же. Когда <body> обработчик выполняется, он ожидает ответ API и добавляет в начало <script> тег, содержащий сериализованные данные.
Если вызов API завершается ошибкой, оболочка всё равно загружается, а SPA переключается на получение данных на стороне клиента.
// 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;
}
}Вариант 2: SPA, размещенное на внешнем origin-сервере
Используйте этот вариант, если ваши HTML, CSS и JavaScript развёрнуты вне Cloudflare. Worker получает оболочку SPA с внешнего источника, с помощью HTMLRewriter внедряет данные инициализации и передаёт изменённый ответ в браузер потоком.
Настройка Worker
Поскольку SPA не находится в Workers Static Assets, вам не нужен assets блок. Вместо этого сохраните URL внешнего источника в переменной окружения. Подключите Worker к своему домену с помощью Custom Domain или Маршрут.
{
"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"Внедрение bootstrap-данных с помощью HTMLRewriter
Worker параллельно загружает и оболочку SPA, и данные API. Когда источник SPA отвечает, HTMLRewriter передает HTML потоком, одновременно внедряя начальные данные (bootstrap data) в <body>. Статические ресурсы (CSS, JS, изображения) передаются на внешний источник без изменений.
// 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;
}
}Используйте предварительно загруженные данные в вашем SPA
На клиенте считайте window.__BOOTSTRAP_DATA__ перед выполнением любых вызовов API. Если данные существуют, используйте их напрямую. В противном случае выполните обычный 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} />;
}Добавьте объявление типа, чтобы TypeScript распознавал глобальное свойство:
declare global {
interface Window {
__BOOTSTRAP_DATA__?: unknown;
}
}Дополнительные методы внедрения
Можно выстраивать цепочку из нескольких обработчиков HTMLRewriter, чтобы внедрять не только данные bootstrap.
Настройка мета-тегов
Внедрите Open Graph или другие <meta> теги в зависимости от пути запроса. Это позволяет краулерам социальных сетей получать корректные превью без полноценного фреймворка серверного рендеринга.
new HTMLRewriter()
.on("head", {
element(el) {
el.append(`<meta property="og:title" content="${title}" />`, {
html: true,
});
},
})
.transform(shell);Добавление CSP-nonce
Генерируйте nonce для каждого запроса и добавляйте его как в заголовок Content-Security-Policy, так и в каждый встроенный <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;Внедрение пользовательской конфигурации
Передавайте флаги функций или настройки, специфичные для окружения, в SPA без дополнительного обращения к 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);Дополнительные материалы
- HTMLRewriter : потоковый парсер и трансформатор HTML.
- Workers Static Assets : раздавайте статические файлы вместе со своим Worker.
- Маршрутизация Static Assets : настройте
run_worker_firstиnot_found_handling. - Привязка Static Assets : справочник по
ASSETSпривязки и параметры маршрутизации. - Custom Domains : подключите Worker к домену как источник.
- Маршруты : запустите Worker перед существующим origin сервером.
- Рекомендации по работе с Workers : шаблоны кода и рекомендации по настройке для Workers.