Dynamic Workers は、Worker のコードと一緒に、HTML ページ、JavaScript バンドル、画像などの静的アセットを配信できます。Dynamic Worker でフルスタックアプリケーションを配信したいときに使います。
Dynamic Workers の静的アセットは、通常の Workers の静的アセット とは動きが異なります。デプロイ時にアセットをアップロードするのではなく、実行時に Worker Loader の get() コールバックで渡し、R2、KV、または別のストレージバックエンドから取得します。
Dynamic Workers の静的アセットを設定する要素は 3 つです。
- アセットを保存する — 静的ファイルを KV 名前空間へアップロードし、プロジェクト ID とパス名でキーを分けます。
- ローダー Worker でアセットバインディングを定義する — KV から静的ファイルを読み、正しいヘッダーを付けて返すクラスを作ります。
- バインディングを Dynamic Worker へ渡す — Dynamic Worker は
env.ASSETS.fetch(request)を呼んで静的ファイルを配信します。
静的アセットは KV 名前空間に保存し、プロジェクト ID で分けます。各プロジェクトのファイルは互いに隔離されます。
project/{projectId}/assets/index.html → file content
project/{projectId}/assets/app.js → file content
project/{projectId}/manifest → asset manifestユーザーがプラットフォームのアップロード API でプロジェクトをデプロイしたら、各ファイルをパス名の下に KV へ保存します。
await env.KV_ASSETS.put(`project/${projectId}/assets${pathname}`, fileContent);マニフェストも保存する必要があります。マニフェストは、どのファイルが存在し、それぞれのコンテンツタイプが何かをアセットハンドラーへ伝える対応表です。@cloudflare/worker-bundler の buildAssetManifest() を使い、アセットから生成します。
import { buildAssetManifest } from "@cloudflare/worker-bundler";
const assets = {
"/index.html": htmlContent,
"/app.js": jsContent,
"/style.css": cssContent,
};
const manifest = await buildAssetManifest(assets);
await env.KV_ASSETS.put(
`project/${projectId}/manifest`,
JSON.stringify(manifest),
);import { buildAssetManifest } from "@cloudflare/worker-bundler";
const assets = {
"/index.html": htmlContent,
"/app.js": jsContent,
"/style.css": cssContent,
};
const manifest = await buildAssetManifest(assets);
await env.KV_ASSETS.put(
`project/${projectId}/manifest`,
JSON.stringify(manifest),
);アセットを保存した KV 名前空間へのアクセスを、ローダー Worker に与えます。
{
"worker_loaders": [{ "binding": "LOADER" }],
"kv_namespaces": [
{
"binding": "KV_ASSETS",
"id": "<your-kv-namespace-id>",
},
],
}[[worker_loaders]]
binding = "LOADER"
[[kv_namespaces]]
binding = "KV_ASSETS"
id = "<your-kv-namespace-id>"ローダー Worker で WorkerEntrypoint を継承するクラスを作り、fetch() メソッドを定義します。WorkerEntrypoint により、Dynamic Worker から RPC でこのメソッドを呼べます。Dynamic Worker が env.ASSETS.fetch(request) を呼ぶと、このメソッドがローダー Worker で実行されます。KV バインディングとアセット配信のロジックは、こちらに置きます。
クラスは projectId という prop を受け取り、どのプロジェクトのアセットを探すかを決めます。fetch() が呼ばれると、次を行います。
- プロジェクトのアセットマニフェストを KV から読み込みます。
- リクエストのパス名をファイルへ解決します。
- ファイルの内容を KV から取得します。
- 正しい
Content-Typeヘッダーを付けたResponseを返します。
リクエストパスとファイルの照合、コンテンツタイプの判定、キャッシュヘッダーの設定を自分で書く代わりに、@cloudflare/worker-bundler パッケージで静的アセット配信を処理できます。fetch() メソッドでは、handleAssetRequest() に次の 2 つを渡します。
- マニフェスト: アップロード時に KV へ保存した、パスとコンテンツタイプの対応表です。
buildAssetManifest()で作ります。handleAssetRequest()は、どのファイルが存在し、コンテンツタイプが何かをこれで判断します。 - ストレージオブジェクト:
handleAssetRequest()が KV 名前空間からファイルを読む方法です。メソッドはget(pathname)の 1 つで、指定したファイルパスの内容を読んで返します。
handleAssetRequest() は、マニフェストに一致するファイルがあれば、コンテンツタイプとキャッシュ用の正しいヘッダーを付けて配信します。
import { WorkerEntrypoint } from "cloudflare:workers";
import { handleAssetRequest } from "@cloudflare/worker-bundler";
export class AssetBinding extends WorkerEntrypoint {
async fetch(request) {
const { projectId } = this.ctx.props;
// Load the project's asset manifest from KV
const manifest = await this.env.KV_ASSETS.get(
`project/${projectId}/manifest`,
{ type: "json", cacheTtl: 300 },
);
if (!manifest) {
return new Response("No assets found", { status: 404 });
}
// Storage object — handleAssetRequest calls get() to
// read file content when it needs to serve an asset
const storage = {
async get(pathname) {
return this.env.KV_ASSETS.get(
`project/${projectId}/assets${pathname}`,
{ type: "arrayBuffer", cacheTtl: 86_400 },
);
},
};
const response = await handleAssetRequest(request, manifest, storage);
return response ?? new Response("Not Found", { status: 404 });
}
}import { WorkerEntrypoint } from "cloudflare:workers";
import { handleAssetRequest } from "@cloudflare/worker-bundler";
export class AssetBinding extends WorkerEntrypoint {
async fetch(request: Request) {
const { projectId } = this.ctx.props;
// Load the project's asset manifest from KV
const manifest = await this.env.KV_ASSETS.get(
`project/${projectId}/manifest`,
{ type: "json", cacheTtl: 300 },
);
if (!manifest) {
return new Response("No assets found", { status: 404 });
}
// Storage object — handleAssetRequest calls get() to
// read file content when it needs to serve an asset
const storage = {
async get(pathname: string) {
return this.env.KV_ASSETS.get(
`project/${projectId}/assets${pathname}`,
{ type: "arrayBuffer", cacheTtl: 86_400 },
);
},
};
const response = await handleAssetRequest(request, manifest, storage);
return response ?? new Response("Not Found", { status: 404 });
}
}AssetBinding をエクスポートすると、ローダー Worker の fetch() ハンドラーで ctx.exports から使えるようになります。ctx はハンドラーの第 3 引数で、request と env のあとです。次の手順で、これを Dynamic Worker へ渡します。
get() を呼んで Dynamic Worker を作るとき、env オブジェクトに AssetBinding を含めます。Dynamic Worker はこれを使って静的ファイルを配信します。前の手順で定義した AssetBinding クラスを参照するには、ctx.exports.AssetBinding() を使い、どのプロジェクトのアセットを配信するかを知らせる projectId を prop として渡します。動きはカスタムバインディングと同じです。props でクラスへ情報を渡し、実行時にクラスは this.ctx.props で読みます。
export default {
async fetch(request, env, ctx) {
const projectId = getProjectIdFromRequest(request);
const worker = env.LOADER.get(projectId, async () => {
const serverCode = await loadServerCode(projectId);
return {
mainModule: "index.js",
modules: {
"index.js": { js: serverCode },
},
compatibilityDate: "2026-09-20",
env: {
ASSETS: ctx.exports.AssetBinding({
props: { projectId },
}),
},
};
});
return await worker.getEntrypoint().fetch(request);
},
};export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const projectId = getProjectIdFromRequest(request);
const worker = env.LOADER.get(projectId, async () => {
const serverCode = await loadServerCode(projectId);
return {
mainModule: "index.js",
modules: {
"index.js": { js: serverCode },
},
compatibilityDate: "$today",
env: {
ASSETS: ctx.exports.AssetBinding({
props: { projectId },
}),
},
};
});
return await worker.getEntrypoint().fetch(request);
},
};Dynamic Worker から見ると、ASSETS はバインディングです。AssetBinding に定義したメソッドなので、env.ASSETS.fetch(request) を呼べます。Dynamic Worker がそのメソッドを呼ぶと、ローダー Worker で実行され、AssetBinding クラスが KV からマニフェストとファイル内容を読みます。
Dynamic Worker から見ると、env.ASSETS はほかのバインディングと同じように使えます。ユーザーはサーバーコードを書き、env.ASSETS.fetch() を呼んで静的ファイルを配信します。
// Inside the Dynamic Worker
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Handle API routes directly
if (url.pathname.startsWith("/api/")) {
return Response.json({ hello: "world" });
}
// Everything else — serve static assets
return env.ASSETS.fetch(request);
},
};// Inside the Dynamic Worker
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
// Handle API routes directly
if (url.pathname.startsWith("/api/")) {
return Response.json({ hello: "world" });
}
// Everything else — serve static assets
return env.ASSETS.fetch(request);
},
};Dynamic Worker が env.ASSETS.fetch(request) を呼ぶと、呼び出しは RPC 経由でローダー Worker の AssetBinding へ届きます。AssetBinding はマニフェストでファイルを探し、KV から読みます。Dynamic Worker 側でこれを扱う必要はありません。env.ASSETS.fetch(request) を呼べば、正しいヘッダー付きのファイルが返り、クライアントへ返せます。