Workers Assets
2024年の Birthday Week でDeveloper Platformである Cloudflare Workers に対して様々なアップデートが発表されました。
その中でもJavaScript/Pythonの実行基盤であるWorkersがStaticなアセットのホスティングをサポートしたことは大きなアップデートです。
Cloudflare Pages との違い
従来StaticなアセットのホスティングはPages,動的スクリプトの実行はWorkersという違いが明確に芯材していました。Pagesのfunctions
フォルダにスクリプトを配置した場合Workersが実行されServer Side Rendering (SSR)の実現が可能となっており、これは Pages Functions という名前で呼称されています。
Birthday WeekのアップデートでPagesが持っているいくつかの機能がWorkersに適応されました。過去このブログではStaging機能について纏めました。
StaticなAssetのホスティングにおけるWorkersとPagesの違いはここに纏まっています。CICD周りはまだPagesの方が機能が充実しています。Angular
,Astro
,Docusaurus
,Gatsby
,Next.js
,Nuxt
,Qwik
,Remix
,Solid
,Svelte
が現在サポートされている一覧です。
早速やってみる
1. StaticAssets Only
ではスクリプトなしのStaticアセットのホスティングを作成していきます。
プロジェクトの初期イニシャライズはC3 (create-cloudflare-cli) を用います。
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"
としてスクリプトが宣言されています。
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.toml
のassets = { 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より先にそちらが評価され読み込まれます。
先ほどの環境を修正して以下の状態にします。
export default {
async fetch(request, env) {
const res = await env.ASSETS.fetch(new Request(new URL('/helloworld.html', request.url)));
return res;
}
}
<!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>
<!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>
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.toml
のhtml_handling
によって制御されます。このオプションがデフォルトでは有効化されておりこの場合、アクセス対象のアセットを明示的な指定をおこなわない場合Routingの中で自動で保管されたindex.html
が優先されます。詳しい挙動の制御方法はこちらに記載があります。
Discussion