LoginSignup
4
1

Cloudflare WorkersでもRemix on Honoをしたい🔥

Last updated at Posted at 2024-06-09

Cloudflare WorkersでRemix on Honoを動かしたときのメモ(2024年6月)

完成リポジトリはこちら

作成動機

  • 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用に書き直しました。

できたもののデモ
hono-and-remix-on-workers-development.gif

Cloudflare上の管理画面からも、Workersアプリであることがわかります
image.png

差分はこちら

package.json

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を返す

server.ts
+ 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/cloudflarecreateReqestHandler()を使うようにします。
early returnしているためelse節は不要に思えますが、ないとBuildに失敗します。

Honoから静的ファイルを返す

結論

server.ts
+ 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で静的ファイルを使う方法は、

  1. hono/cloudflare-workersserveStatic()を使う
  2. Cloudflare R2を使う
  3. Cloudflare KVを使う
  4. ファイル自体をimportする

の4つが思いつきます。今回は3のKVを使う方法を用いました。
1は2024年6月現在、honojs/honoにdeprecatedとしてマークされています。(静的ファイルを返すならPagesを使え、というのがCloudflareの方針でしょう。)

4はファイルが増えるたび記述量を増やさなければならないので論外として、インターネット上に(そこそこ)例が転がっている3の方法を用いました。

Cloudflare KVを用いて静的ファイルを返す

@cloudflare/kv-asset-handlergetAssetFromKV()を用います。

基本上記の記事に従って書きますが、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を使ったほうが望ましいかと思いますが、小規模なアプリケーションを構築する場合ならこの程度で十分じゃないかなと思いました。

4
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1