👋

Workers Assets

2024/12/07に公開

2024年の Birthday Week でDeveloper Platformである Cloudflare Workers に対して様々なアップデートが発表されました。
https://blog.cloudflare.com/builder-day-2024-announcements/

その中でもJavaScript/Pythonの実行基盤であるWorkersがStaticなアセットのホスティングをサポートしたことは大きなアップデートです。

Cloudflare Pages との違い

従来StaticなアセットのホスティングはPages,動的スクリプトの実行はWorkersという違いが明確に芯材していました。Pagesのfunctionsフォルダにスクリプトを配置した場合Workersが実行されServer Side Rendering (SSR)の実現が可能となっており、これは Pages Functions という名前で呼称されています。

Birthday WeekのアップデートでPagesが持っているいくつかの機能がWorkersに適応されました。過去このブログではStaging機能について纏めました。
https://zenn.dev/kameoncloud/articles/6b629dedba2b48

StaticなAssetのホスティングにおけるWorkersとPagesの違いはここに纏まっています。CICD周りはまだPagesの方が機能が充実しています。
https://developers.cloudflare.com/workers/static-assets/compatibility-matrix/
またWorkers Assetが正式にサポートしているフロントエンドフレームワークは以下のとおりでありまだPagesに比べると限定的です。Angular,Astro,Docusaurus,Gatsby,Next.js,Nuxt,Qwik,Remix,Solid,Svelte が現在サポートされている一覧です。

早速やってみる

1. StaticAssets Only

ではスクリプトなしのStaticアセットのホスティングを作成していきます。
プロジェクトの初期イニシャライズはC3 (create-cloudflare-cli) を用います。
https://zenn.dev/kameoncloud/articles/549a2e558a2adb
C3はWranglerと異なり主要なフロントエンドフレームワークの実行環境や操作の機能が合わせて提供されます。

npm create cloudflare@latest -- my-static-site --experimental

入力は以下で行います。

╰ What would you like to start with?
  ● Hello World example
---
╰ Which template would you like to use?
  ● Hello World - Assets-only

あとはいつものWorkersと同じです。
作成されたら生成されたURLにアクセスすればHello Worldが表示されます。

wrangler.toml とpublicフォルダ

まず、(当たり前ですが)いつものsrcフォルダが存在していません。

wrangler.tomlは以下のようになっています。

name = "my-static-site"
compatibility_date = "2024-11-27"
assets = { directory = "./public" }

[observability]
enabled = true

[observability]はBirtdayWeekで新たに加わったアップデートの一つでWorekrsは現在WebSocketによる揮発性のあるログだけではなく、マネージメントコンソールから確認&検索可能なログを出力するオプションで、新しく作成する全Workersにおいてデフォルトでオンになっていますが、StaticAssetのみのWorkersはログが記載されません。あくまでスクリプトの実行ログになります。
この記事のポイントはassets = { directory = "./public" }です。StaticAssetが./publicフォルダに格納されているということを意味しています。結果としてアクセスすると./public/index.htmlがデフォルトとして表示されます。

2. Workers スクリプト + StaticAssets

では次にWorkersスクリプトとStaticAssetsの混合状態をDeployします。先ほどと異なり以下のオプションを指定します。

╰ Which template would you like to use?
  ○ Hello World - Assets-only
  ● Hello World - Worker with Assets

Workers のバインディングについて

生成されたwrangler.tomlを見ていきます。

name = "my-static-site"
main = "src/index.js"
compatibility_date = "2024-11-27"
compatibility_flags = ["nodejs_compat"]
assets = { directory = "./public", binding = "ASSETS" }

[observability]
enabled = true

今度はmain = "src/index.js"としてスクリプトが宣言されています。

src/index.js
export default {
	async fetch(request, env, ctx) {
		const url = new URL(request.url);
		switch (url.pathname) {
			case '/message':
				return new Response('Hello, World!');
			case '/random':
				return new Response(crypto.randomUUID());
			default:
				return new Response('Not Found', { status: 404 });
		}
	},
};

対してhtmlは以下です。
```html:index.html
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Hello, World!</title>
	</head>
	<body>
		<h1 id="heading"></h1>
		<button id="button">Fetch a random UUID</button>
		<div id="random"></div>
		<script>
			fetch('/message')
				.then((resp) => resp.text())
				.then((text) => {
					const h1 = document.getElementById('heading');
					h1.textContent = text;
				});

			const button = document.getElementById("button");
			button.addEventListener("click", () => {
				fetch('/random')
					.then((resp) => resp.text())
					.then((text) => {
						const random = document.getElementById('random');
						random.textContent = text;
					});
			});
		</script>
	</body>
</html>

main = "src/index.js"の宣言により最初にindex.jsが起動されます。その後wrangler.tomlassets = { directory = "./public", binding = "ASSETS" }に基づき、const url = new URL(request.url);./publicを呼び出しています。index.htmlはHTMLの世界におけるデフォルトで選択されています。

Routing について

Workers Assets ではStaticなアセットとjsの呼び出し順は以下の関係性となっています。

./public/フォルダのアセットは直下に存在しているものとして直接呼出しが可能です。
呼び出しが失敗した場合(アセットが存在していない場合)not_found_handlingが処理されjsが起動します。後述しますがデフォルトではindex.htmlが存在している場合、jsより先にそちらが評価され読み込まれます。

先ほどの環境を修正して以下の状態にします。

index.js
export default {
	async fetch(request, env) {
		const res = await env.ASSETS.fetch(new Request(new URL('/helloworld.html', request.url)));
		return res;
		}
  }
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index.html</title>
</head>
<body>
    <h1>index.html</h1>
</body>
</html>
helloworl.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>
wrangler.toml
name = "my-static-site"
main = "src/index.js"
compatibility_date = "2024-11-27"
compatibility_flags = ["nodejs_compat"]
assets = { directory = "./public", binding = "ASSETS" }

[observability]
enabled = true

Deployを行いhttps://my-static-site.harunobukameda.workers.dev/helloworld.htmlにアクセスするとhelloworld.htmlが表示されます。この際.htmlは省略されます。一方https://my-static-site.harunobukameda.workers.dev/にアクセスを行った場合index.htmlが表示されます。先ほどのRoutingの図だとindex.htmlを明示的に指定していないため、以下の部分が評価されhelloworld.htmlが表示されるはずですが、index.htmlが表示されます。

		const res = await env.ASSETS.fetch(new Request(new URL('/helloworld.html', request.url)));

試しにindex.htmlをpublicフォルダから削除するかリネームしてdeployを行うとhelloworld.htmlが表示されるためScriptは正しいことがわかります。

html handling

wrangler.tomlの以下の部分を修正して再度Deployします。

assets = { directory = "./public", binding = "ASSETS", html_handling = "none" }

今度はindex.htmlが存在していたとしても想定通りhelloworld.htmlが表示されます。この挙動はwrangler.tomlhtml_handlingによって制御されます。このオプションがデフォルトでは有効化されておりこの場合、アクセス対象のアセットを明示的な指定をおこなわない場合Routingの中で自動で保管されたindex.htmlが優先されます。詳しい挙動の制御方法はこちらに記載があります。
https://developers.cloudflare.com/workers/static-assets/routing/

Discussion