作業ログです。
Remix のアプリを Cloudflare Pages にデプロイして KV にアクセスするまでをやってみました。
結果としては、id:leader22 さんの記事*1と同じところにハマりました。 こちらの記事も参考になるかと思います。
違いはTypeScript用に型定義を用意したくらいでしょうか。
create-remix
とりあえず、脳死でcreate-remix
していきます。
> npx create-remix@latest` > Just the basics > Cloudflare Pages > TypeScript > Run npm install ? > n
yarn を使いたいのでnpm install
は断っておきます。
さて、テンプレートが展開されて完成しました。よかったですね。
とはいえ、このままyarn dev
やyarn build
を行うとNo matches found: "dev:*"
のように怒られてしまいますので修正しておきます。
- "build": "run-s build:*", + "build": "run-s 'build:*'", - "dev": "remix build && run-p dev:*", + "dev": "remix build && run-p 'dev:*'",
KV access in Pages Functions
間違ったやり方
私が躓いた、Cloudflare Pages にとって間違っていた方法です。 「remix cloudflare kv」などで調べると、その多くが Cloudflare Workers での実装なのでそのまま写すと動いてくれません。
Remix のドキュメントでは KV の名前をそのまま呼べると書かれており、加えてそれ用の型定義を宣言しておくことをオススメされます。
例えば KV の名前がGOOD_KVNAME
だとすると、実装イメージは以下のような感じです。
// global.d.ts declare const GOOD_KVNAME: KVNamespace // app/routes/index.tsx export const loader: LoaderFunction = async () => { const cached = await GOOD_KVNAME.get<Cache>('cache-key', 'json') ... }
RemixをCloudflare Workersで動かす & KVでデータをキャッシュする
しかし、これらは Cloudflare Pages の環境では動きません。ではどうしたら良いのでしょうか?
正しいやり方
まずはローカルで実行するために、package.json
にあるdev
スクリプトに手を加えます。
https://developers.cloudflare.com/pages/platform/functions/#develop-and-preview-locally
ドキュメントに従って、静的アセットのディレクトリを指定した後ろにKV_NAMESPACE
を渡します。
package.json - "dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public", + "dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public -k GOOD_KVNAME",
ローカルでの準備が終わったので、yarn dev
で開発環境を立ち上げてみます。すると、以下のようなエラーが返ってきます。
You must use the 2nd `env` parameter passed to exported handlers/Durable Object constructors, or `context.env` with Pages Functions.
どうやら、Pages Functions から KVNamespace を指定するには context.env
から取得する必要があるようです。
プロジェクトルートにある、テンプレートから生成されたserver.js
を見てみると以下のような実装になっています。
この実装から、ルーティングファイルにあるloader
に渡ってくるリクエストのcontext
にcontext.env
の値が入っているのが分かります。
// server.js const handleRequest = createPagesFunctionHandler({ build, mode: process.env.NODE_ENV, getLoadContext: context => context.env, })
つまり、ルーティングファイルのloader
に渡ってくる context から、以下のように呼び出すことが出来ます。
const loader: LoaderFunction = async ({context}) => { const kv = context.GOOD_KVNAME as KVNamespace ... }
とはいえ、context: any
を無理やりKVNamespace
へキャストするのもムズムズするので型定義を用意します。
// global.d.ts import type {AppData, DataFunctionArgs} from '@remix-run/cloudflare' interface LoaderFunctionArgs extends Omit<DataFunctionArgs, 'context'> { context: { GOOD_NAMESPACE: KVNamespace } } type LoaderFunctionResult = Promise<Response> | Response | Promise<AppData> | AppData declare global { type LoaderFunction = (args: LoaderFunctionArgs) => LoaderFunctionResult }
型定義を用意したことで、キャストの必要がなくなり以下のような実装になりました。
// app/routes/index.tsx import {useLoaderData} from '@remix-run/react' type Cache = { a: number } type LoaderData = { pageCache: Cache } export const loader: LoaderFunction = async ({context}): Promise<LoaderData> => { const kv = context.GOOD_KVNAME const cached = await kv.get<Cache>('cache-key', 'json') const updateCache = {...cached, a: (cached?.a ?? 0) + 1} await kv.set('cache-key', updateCache) return updateCache }
以上でとりあえずは動くようになりました。
やっと開発が始められます。やったね。