こんにちは、ミツカリCTOの塚本こと、つかびー(@tsukaby0) です。
ミツカリではPaaSには専らAWSを利用しており、Cloudflareはほとんど利用していません。また、私自身もCloudflareを過去がっつり触ってきた経験はないため、Cloudflareに入門(学習)してみることにしました。
Cloudflare WorkersとPages
Cloudflareは元々DNS(セキュリティ)屋でしたが、その後CDNのサービスを備えて有名になり始め一般利用者を増やし、近年ではホスティングやコンピューティングのサービスを提供する総合的なPaaSに進化してきました。
Cloudflare Workersはサーバーレスのコンピューティングサービスとして2017年にローンチされました。以下の通り当時のblogが残っていますね。
その後、2020年にJAM stackサイトをホスティングできるサービスであるCloudflare Pagesがローンチされました。
WorkersとPagesの違い
WorkersもPagesもどちらもWebサイトやWebアプリケーションを配信するためのサービスですが、違いとしてはサーバー上でプログラムを動かすことができるか、という点が異なります。
詳細は以下のさくさくさんの記事が参考になります。
迷ったら「静的 → Pages」「動的 → Workers」「両方 → Pages + Functions」
とあるように素のHTML+CSSだけで済むようなWebサイトはPagesを使うと良いですし、APIやデータ操作などの動的処理が必要なWebアプリだとWorkersを使うのが良いです。一部のブラウザだけで完結するようなWebアプリはPagesでも事足りますので、ユースケース次第です。
しかし、基本的にこれからは ユースケースに限らず、Workersを使う と記憶しておくと良いです。
理由については以下の公式の記事の通りです。
Workers will receive the focus of Cloudflare's development efforts going forwards, so we therefore are recommending using Cloudflare Workers over Cloudflare Pages for any new projects ↗.
CloudflareはこれからWorkersの開発に集中するし、Workersの利用を推奨すると言っています。実際に同ページ下部にあるマトリクスを見るとPagesでできることはほぼ全てWorkersでできます。つまり単なる静的なWebサイトでもWorkersで配信できます。
現状のUIでは分かれていたWorkersとPagesが同じページに集まっています。

Cloudflare Pagesの中にあるFunctionsも実体としてはWorkersのようですし、今後は特に区別したり意識する必要は無くなるかもしれませんが、現時点ではWorkersの利用が推奨されていることを念頭に置いておくと良さそうです。
Cloudflareアドボケイトのyusukebeさんの記事も参考になります。
https://yusukebe.com/posts/2024/cloudflare-workers-updates/
React + Vite
Workersを使って何らかのWebサイトまたはWebアプリを配信するためのドキュメントは公式が多数用意しています。
今回はReact + ViteでSPAを作成し、Workersにデプロイしてみます。と言っても、これはすでにテンプレートが用意されていますし、公式サイトにも解説があるため、立ち上げてデプロイするだけであればそれほど苦労はしないと思います。
React + Vite · Cloudflare Workers docs
基本的にはこれに従えば良いですが、以下の通りNEKOYASANが既に試している記事があるため、そちらも参考にされると良いかと思います。
こちらの記事と同じことを書いてもつまらないので、もう少し踏み込んだ話をしていきます。
Viteでの動作確認
上記の記事に従って初期化し、 yarn dev で起動した場合、port 5173で以下のアプリが動きます。

count is の部分は以下のようなコードになっており、ReactのuseStateでボタンを押した回数が保持・表示されます。つまりブラウザ側でJSが動いています。
<button
onClick={() => setCount((count) => count + 1)}
aria-label='increment'
>
count is {count}
</button>
その下にある Name from API is: unknown のボタンは押すと Name from API is: Claudeflare に変わります。

これは worker/index.ts のAPIが動いており、そこにリクエストが飛んでいるためボタンのテキストが変わります。
worker/index.ts のコードは以下のようになっています。単純ですね。特定のパスのアクセスの場合、jsonを返しているだけです。
export default { fetch(request) { const url = new URL(request.url); if (url.pathname.startsWith("/api/")) { return Response.json({ name: "Cloudflare", }); } return new Response(null, { status: 404 }); }, } satisfies ExportedHandler<Env>;
Workersにおけるルーティングを理解する
動作確認はできましたが、実際にアプリケーションを開発していくならばCloudflare Workersにおけるルーティングを理解しておいた方が良いです。その辺りについて触れていきます。
Sec-Fetch-Mode: navigate
まず、前述の動作確認のとおり、 /api/* にアクセスするとWorkerのコード(API)が動くと言いましたが、実際にそこ単体で動作確認をしてみましょう。ブラウザのURLに http://localhost:5173/api/ と入れてアクセスします。するとどういうわけかHTMLが返ってきます。

先ほどの動作確認ではjsonが返ってきていましたが、どうして違うのでしょうか。curlでも試してみますが、curlでは以下の通りjsonが返ります。
$ curl http://localhost:5173/api/
{"name":"Cloudflare"}%
この謎については以下の資料が参考になります。
By enabling this, the platform assumes that any navigation request (requests which include a Sec-Fetch-Mode: navigate header) are intended for static assets and will serve up index.html whenever a matching static asset match cannot be found.
https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/
When you configure single-page-application mode, Cloudflare provides default routing behavior that automatically serves your /index.html file for navigation requests (those with Sec-Fetch-Mode: navigate headers) which don't match any other asset.
(curlと違って)ブラウザは Sec-Fetch-Mode: navigate というリクエストヘッダを自動で付けます。これはCloudflare特有の話ではなくW3C(ブラウザ)の仕様です。
Cloudflare側では Sec-Fetch-Mode: navigate ヘッダがあれば画面遷移だと捉えて、index.htmlを返す、そうでなければ worker/index.ts を動かす、という仕組みになっています。そのため、このような違いが生まれるのです。
callbackなどの処理をworkerで処理したい
workerに定義する処理が単なるAPIであれば、ほとんどはfetchやXHRによってリクエストされるため、 Sec-Fetch-Mode: navigate ヘッダは付きません。そのため、index.htmlが返ってくることはなく、問題は起きません。
ただし、workerに何らかのcallback処理、特にauth関係の処理を定義したい場合などは問題が起きます。「APIではなく、このURLにアクセスしたら特定のbackend処理を動かして/fooにリダイレクトしたい」というようなケースです。そういうコードをworkerに書きたくなりますが、前述の通り通常のブラウザの画面遷移では Sec-Fetch-Mode: navigate ヘッダが付いてしまうため、index.htmlが返りworkerが動いてくれません。
ではどうすればいいかというと、以下の公式サイトが参考になります。
[assets] directory = "./dist/" not_found_handling = "single-page-application" binding = "ASSETS" run_worker_first = [ "/api/*", "!/api/docs/*" ]
run_worker_first を使います。
CloudflareのSPAのルーティングではまず static resourceが解決されるのがデフォルトですが、そうではなく初めにworkerを動かしたいというケースもあります。そういう時にこの設定を使います。これによって Sec-Fetch-Mode: navigate に関係なく、特定のパスのアクセスはworkerを動かすことができます。
Workersにおけるルーティングの順序
これも公式docに記載があります。Cloudflareはドキュメントが結構揃っており素晴らしいですね。
同ページの下部にあります(Full routing decision diagramの部分)。
https://developers.cloudflare.com/workers/static-assets/routing/single-page-application/#reference
図を見た方が早いですが、 run_worker_firstチェック -> 一致する場合はworkerを動かす -> 一致しない場合はnavigateかどうかをチェック -> navigateの場合はstatic asset、そうでないならworker というようなフローになっています。
注意点
先ほどのURLに not_found_handling の説明もあるため、これも必要に応じて読んでおくと良いです。
wrangler.tomlの設定のcompatibility_dateによって挙動が変わるのでこの辺りは要注意です。
また、今回はあくまでSPAでの設定、挙動の話なので、SSRだけで作るような場合はまた設定や一部の前提が変わるため注意が必要です。
おわり
React + Viteのサンプルを作りつつ、ルーティングの仕組みを解説してみました。
サンプルアプリにAuth0の認証を加えたりD1を追加したりHonoを入れたり、実は色々やっているのですが、その辺りまで含めると長くなってしまうので今回はルーティング周りを重点的に解説する記事にしてみました。
実際に開発する中でルーティングの理解が曖昧なせいで結構苦労しました。生成AIも的確なコードを出してこなかったり、別のいまいちなコードを出してきたりしたので、この記事が誰かの役に立てば幸いです。
現在、ミツカリではITエンジニアを募集しています。興味のある方はぜひお気軽にご連絡ください!