2024年11月現在、yusukebeさんのhono-remix-adapterで同じことができます。(筆者もコントリビュートしています。)
Cloudflare WorkersでRemix on Honoを動かしたときのメモ(2024年6月)
この記事はアーカイブとして残しております。
同じことをやりたい方はhono-remix-adapterをご利用ください。
完成リポジトリはこちら
作成動機
- Remix on Honoをやってみたかった
- Cloudflareを使ってアプリケーションを作りたかった
- フロントエンドとバックエンドを両方使いたかった
- バックエンドにおいて、Cloudflare Pagesではしきれないような処理をしたかった
- Pages + Workersのように分けるには冗長なように感じた
- 個人でサクッと開発したかった
執筆動機
Remix and Hono on Viteはyusukebeさんが作成してくださっています。
こちらはCloudflare Pagesで動作しますが、これをWorkersに移植した際つまづきポイントがいくつかあったため、執筆しました。
remix-honoというリポジトリも存在し、2024年6月時点でアクティブなリポジトリであるものの、かゆいところに手が届かなかったので今回はyusukebeさんのリポジトリからフォークして作成しました。
目指すゴール
- 開発環境(ローカル)でのホットリロード可
- 開発環境(ローカル)と本番環境(Workers)での差分が無い状態
- フロント / バックはもちろん、静的ファイルのレスポンスもやる
- 環境変数による分岐(
if
文)は許容
やったこと
前述のyusukebe/hono-and-remix-on-viteをWorkers用に書き直しました。
Cloudflare上の管理画面からも、Workersアプリであることがわかります
package.json
"scripts": {
+ "deploy": "wrangler deploy server.ts",
+ "start" : "wrangler dev server.ts",
- "deploy": "wrangler pages deploy",
- "start" : "wrangler pages dev ./build/client",
PagesではなくWorkersにデプロイするため、デプロイコマンドを変更します。
HonoからRemixを返す
+ import { type AppLoadContext, createRequestHandler } from '@remix-run/cloudflare'
+ import * as build from './build/server'
- import { type AppLoadContext } from '@remix-run/cloudflare'
- import { staticAssets } from 'remix-hono/cloudflare'
// 中略
+ app.all('*', async(c) => {
- app.use(async(c, next) => {
+ const remixContext = {
+ cloudflare: { env: c.env }
+ } as unknown as AppLoadContext
if (process.env.NODE_ENV !== 'development' || import.meta.env.PROD) {
+ // production
+ const handleRemixRequest = createRequestHandler(build, 'production')
+ return await handleRemixRequest(c.req.raw, remixContext)
- return staticAssets()(c, next)
- }
+ } else {
+ if (!handler) {
+ // @ts-expect-error it's not typed
+ const build = await import('virtual:remix/server-build')
+ const { createRequestHandler } = await import('@remix-run/cloudflare')
+ handler = createRequestHandler(build, 'development')
+ }
+ return handler(c.req.raw, remixContext)
+ }
- await next()
},
// 後略
フロントエンドのレスポンスを返す際、Static Assetsではなく@remix-run/cloudflare
のcreateReqestHandler()
を使うようにします。
early returnしているためelse節は不要に思えますが、ないとBuildに失敗します。
Honoから静的ファイルを返す
結論
+ import { getAssetFromKV } from '@cloudflare/kv-assert-handler'
// 中略
+ app.on(
+ 'GET',
+ ['/assets/*', '/favicon.ico'], //assets配下とfaviconを静的ファイルとして指定
+ async (c) => {
+ if (process.env.NODE_ENV !== 'development' || import.meta.env.PROD) {
// if文の中でimportしないと、ローカルでは`Cannot find module`エラーになる
+ const manifest = await import('__STATIC_CONTENT_MANIFEST')
+ try {
+ return await getAssetFromKV({
+ request: c.req.raw,
+ waitUntil: c.executionCtx.waitUntil.bind(c.executionCtx)
+ }, {
+ ASSET_NAMESPACE: c.env.__STATIC_CONTENT,
+ ASSET_MANIFEST: JSON.parse(manifest.default),
+ })
+ } catch (e) {
+ return c.notFound()
+ }
+ }
+ }
+ )
方針
hono + Cloudflare Workersで静的ファイルを使う方法は、
-
hono/cloudflare-workers
のserveStatic()
を使う - Cloudflare R2を使う
- Cloudflare KVを使う
- ファイル自体をimportする
の4つが思いつきます。今回は3のKVを使う方法を用いました。
1は2024年6月現在、honojs/hono
にdeprecatedとしてマークされています。(静的ファイルを返すならPagesを使え、というのがCloudflareの方針でしょう。)
4はファイルが増えるたび記述量を増やさなければならないので論外として、インターネット上に(そこそこ)例が転がっている3の方法を用いました。
Cloudflare KVを用いて静的ファイルを返す
@cloudflare/kv-asset-handler
のgetAssetFromKV()
を用います。
基本上記の記事に従って書きますが、Honoを使うにあたって下記の点に注意してください。
上記の例では第一引数が
{
request,
waitUntil: ctx.waitUntil.bind(ctx)
}
となっていますが、Honoを使う場合は
{
request: c.req.raw,
waitUntil: c.executionCtx.waitUntil.bind(c.executionCtx)
}
となります。
今回はローカル環境で読み込めない__STATIC_CONTENT_MANIFEST
を本番環境のみでimportしているのですが、その際のimport文が
const manifest = await import('__STATIC_CONTENT_MANIFEST')
となること、ASSET_MANIFEST
の取り出し方が
- JSON.parse(manifest)
+ JSON.parse(manifest.default)
のようにdefault
が追加されることに注意してください。
ファイルの置き場所
静的ファイルの置き場所はwrangler.toml
に書きます。
筆者は
[site]
bucket="./build/client
のようにして、ビルド時に吐き出されるパスを指定しました。
備考 getAssetFromKV()
の挙動について
$ tree .
.
├── assets
│ └── hono-logo.png
└── favicon.ico
上記のように設定した場合、Cloudflare KVには
{
Key: assets/hono-logo.2ba06455c3.png
Value: `This value contains unprintable characters. Download to view.`
}, {
Key: favicon.dfd986c849.ico
Value: `This value contains unprintable characters. Download to view.`
}
のようにして登録されます。
favicon.ico
に対してリクエストが飛んだ場合、getAssetFromKV()
関数は第2引数のASSET_MANIFEST
に渡された値(今回はJSON.parse(manifest.default)
)からPATHであるfavicon.ico
とKeyであるfavicon.dfd986c849.ico
の対応関係を読み取りValueを返却しているようです。
そのため、npm run deploy
実行時にTerminalへ表示されるファイル名がローカルのものと異なっていますが、PATHは変わらずリクエストすることができます。
このことはJSON.parse(manifest.default)
をconsole文で出力すると分かりました。
おわりに
Cloudflare workersでHono + Remixを静的ファイル含めて立ち上げることができました。
静的ファイルを大量に返す場合は素直にCloudflare Pagesを使ったほうが望ましいかと思いますが、小規模なアプリケーションを構築する場合ならこの程度で十分じゃないかなと思いました。