跳转到内容
搜索文档

静态资产

最后更新 查看 MarkdownAgent 设置

Dynamic Workers 可以与您的 Worker 代码一起提供静态资产,例如 HTML 页面、JavaScript 包、图像和其他文件。当您需要 Dynamic Worker 来提供全栈应用程序时,这非常有用。

Dynamic Workers 的静态资产与 常规 Workers 中的静态资产 工作方式不同。您无需在部署时上传资产,而是通过 Worker Loader 的 get() 回调在运行时提供它们,从 R2、KV 或其他存储后端获取。

运作方式

为 Dynamic Workers 设置静态资产有三个部分:

  1. 存储资产 — 将静态文件上传到 KV 命名空间,由项目 ID 和路径名作为键。
  2. 在 loader Worker 中定义资产绑定 — 创建一个处理静态文件请求的类,通过从 KV 中读取文件并返回正确的标头。
  3. 将绑定传递给 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);

您还需要存储一个清单 (manifest),这是一个告知资产处理程序哪些文件存在及其内容类型是什么的映射。使用 @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),
);

向 loader Worker 添加绑定

授予 loader Worker 访问您存储资产的 KV 命名空间的权限:

{
	"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>"

定义资产绑定

在扩展了 WorkerEntrypoint 的 loader Worker 中创建一个类,并定义一个 fetch() 方法。WorkerEntrypoint 使得此方法可以从 Dynamic Worker 使用 RPC 进行调用。当 Dynamic Worker 调用 env.ASSETS.fetch(request) 时,它将在 loader Worker 中运行此方法,KV 绑定和您的资产提供逻辑都存在于此。

该类采用 projectId prop,以便它知道要查找哪个项目的资产。当调用 fetch() 时,它:

  1. 从 KV 加载项目的资产清单。
  2. 将请求路径名解析为文件。
  3. 从 KV 中获取文件内容。
  4. 返回具有正确 Content-Type 标头的 Response

使用 @cloudflare/worker-bundler 处理静态资产服务

无需编写您自己的逻辑来匹配请求路径到文件、检测内容类型并设置缓存标头,而是使用 @cloudflare/worker-bundler 包来处理静态资产服务。在您的 fetch() 方法中,将两样东西传递给 handleAssetRequest()

  • 清单,您在上传期间存储在 KV 中的路径到内容类型映射,使用 buildAssetManifest() 构建。这告诉 handleAssetRequest() 存在哪些文件及其内容类型。
  • 存储对象,告诉 handleAssetRequest() 如何从您的 KV 命名空间中读取文件。它有一个方法 get(pathname),用于读取并返回给定文件路径的内容。

如果 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 后,它将在 loader Worker 的 fetch() 处理程序中的 ctx.exports 上可用。ctx 是处理程序的第三个参数,位于 requestenv 之后。这就是您在下一步中将其传递给 Dynamic Worker 的方法。

将资产绑定传递给 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-08-17",
				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 视为一种绑定,并可以调用 env.ASSETS.fetch(request),因为这就是您在 AssetBinding 上定义的方法。当 Dynamic Worker 调用该方法时,它在 loader Worker 中运行,您的 AssetBinding 类在其中从 KV 读取清单和文件内容。

在 Dynamic Worker 中使用资产绑定

从 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 发送到 loader Worker 的 AssetBinding,其在清单中查找文件并从 KV 中读取。Dynamic Worker 不需要处理任何这些 — 它调用 env.ASSETS.fetch(request) 并获得带有正确标头的文件,准备返回给客户端。

这篇文档对您有帮助吗?