端的にいうと、
- フロントエンドはSvelteKitやらモダンなやつで組んで
- Cloudflare Pagesにデプロイしたい
- そしてKVやD1やらも使いたいし
- ローカルでも実際の値を参照して開発したい
つまり、サーバーレンダリングやAPIルートを実装するときに、既存のスタックに保存してある値を使いたいという話。
個人的にはあるあるのケースで、あらゆるものをCloudflareのエッジで完結させる未来を待つなら、なおさら。
ローカルから実際のKVやD1にアクセスするには
現状、これをやるには2通りの方法しかない。
まず前者。これはいわずもがな、HTTP経由でアクセスできる。
ただ、Cloudflare Pagesにデプロイするなら、Workersで動作するコードからアクセスするなら、あえて1クッションはさむ理由はなさそう。
つぎに後者だが、これはPagesではなくWorkers単体の構成でしか使えない。
ならPagesやめてWorkers Sitesにするか?っていうと、そうはならない(なれない)ことのほうがおおいはず。
GitHubとの連携も、CIでのビルドも、ブランチプレビューも、何もかも失っちゃう。なので、Workers Sitesのことはいったん忘れる。
Pagesにも、`wrangler dev pages`コマンドがある・・・が、これはローカルに閉じてる。`--remote`なオプションも現状は存在しない。
(そしてこのコマンドの場合、`wrangler pages dev --kv=XXX -d=YYY`みたいな渡し方でしか動かなくて不便だったはず)
Viteベースのフレームワークの課題
たとえば、SvelteKitの場合。
最近のメタフレームワークは、Viteに乗っかる形で実装されてることが多いので、`vite dev`みたいなコマンドでローカルで動作させることがほとんど。
が、`vite dev`では、デプロイされるプラットフォーム固有のオブジェクト(SvelteKitでいう`platform.env`)に値が入らないので、ランタイムでKVなんかにアクセスできない。
`wrangler pages dev --kv=XXX -- npm run dev`は、一見惜しく感じるかもしれないが、同様に`undefined`なまま。それにできたとしてもローカル完結。
あれこれやってみた結果、
# これらを同時に vite build --watch wrangler pages dev .svelte-kit/cloudflare --kv=XXX --live-reload
という合せ技で、とりあえずローカルで動く状態にはできる。が・・・、このパターンは、毎回`vite build`でフルビルドするので、とにかくDXが悪い。
コードを更新する度に3秒待つし、画面もフルリロードする。もっとも手数が少ないやり方ではあるものの、何年前なんだ・・ってなる。
SvelteKitの場合、Hooksという仕組みを使って、`platform`オブジェクトごと再実装するっていう荒業も見出されてた。
// hooks.server.js import { dev } from "$app/environment"; export const handle = async ({ event, resolve }) => { if (dev) event.platform = { /* Mock here */ }; return resolve(event); };
ここの実装をなんとかしてでっち上げれば、`vite dev`だけで動かせるようになって、DXが少しは改善される。
適当なモックの実装でローカル完結させるもよし、REST APIにつないで本番データにつなぐもよし。
がんばってモックしてる先人のコードはこちら。
https://github.com/sveltejs/kit/issues/4292#issuecomment-1550596497
APIを使う場合は、がんばって`fetch`ラッパーを書く必要がある。現状、D1のAPIはまだ公開されてなさそうではあるが、内部的には存在する気配を感じるので、そのうちできるようになりそう。
がんばってモックするパターンは、KVのREADだけあればいいとか、接点が小さいならばありかもしれんけど、エッジに生きるものとしてはやっぱ使い倒したいよな〜って。
他のメタフレームワークは
SvelteKitでなくても、Viteベースのメタフレームワークを使う場合、おそらく同じ結果になるはず。
- SolidStart
- Astro
- QwikCity
少なくともこのあたりは。
Next.jsは、そもそも`@cloudflare/next-on-pages`っていうそれ用のレイヤーがないとまともに立てもしない状態な上、READMEを見る限り`wrangler pages dev`を使うらしいので、実データは参照できなそう。
Remixは、たしかViteベースではなかったはずやけど、最新のテンプレを見る限り`wrangler pages dev`を使うようだった。
コード更新からの反映速度が気にならず、ローカル完結でよければ、React界隈のほうが幸せになれるんかね。(未検証)
いっそのこと、別のWorkerを用意する
という割り切りもあるよって話。
これは単純で、
- そもそもPagesにデプロイするアプリ(今回ならSvelteKit)側で、KVに直アクセスするのをやめる
- Cloudflare Workersの別プロジェクトでAPIを作る
- こっちでKVやD1にアクセスする
- こっちのプロジェクトで`wrangler dev --remote`
- SvelteKit側からは通常の`fetch`で読むようにする
とすれば、実データにもアクセスできるし、コードの反映もすぐのまま。
問題があるとすれば、
- PagesとWorkersの2つを別で管理することになる
- デプロイも2倍
- SK側でAPIを作る手段があるのに、わざわざ1クッション
- パフォーマンス的にもこの1RTTは余計
- コードの書き味としても直感的ではない
というあたり。
悩ましいが、債務分離という意味では妥当という見方もできなくはない。(世のトレンドがそうさせてくれんかもしれんけど)
そういうわけでライブラリ作った
前フリが長くなったけど、
- SvelteKitのようなフレームワークを`vite dev`で快適にローカル開発しながらも
- 実際のCloudflareスタックにアクセスしたい
場合は、モックの実装を自分で差し込むしかなさそう。
ならば、せめて、そのモックを楽に使えるようにしようと作ったのがコレ。
簡単にいうと、
というもの。
import { createBridge } from "cfw-bindings-wrangler-bridge"; /** @type {import("@cloduflare/workers-types").KVNamespace} */ const MY_KV = createBridge("http://127.0.0.1:8787").KV("MY_KV"); // ✌️ This is real KV! await MY_KV.put("foo", "bar"); await MY_KV.get("foo"); // "bar"
アプリ側のコードとしてはこれだけでいい。
- アプリ: 実APIと同じコードを呼ぶ
- 🌉モジュール: HTTPリクエストに変換して、ブリッジワーカーへ投げる
- 🌉ワーカー: HTTPリクエストを解釈して実APIを呼んで、レスポンス
- 🌉モジュール: レスポンスを実APIの返り値に変換して返す
- アプリ: 実APIと同じ結果が取得できる!
`wrangler dev`をそのまま使ってるので、
- `--remote`を外せばそのままローカル環境も使える
- `wrangler pages dev`相当
- `wrangler`自体がアップデートされても、それは使う側が選べる
- アプリ内のコードが汚れない
- ローカルなURLだけ管理すればいい
というあたりがポイントかも。
最初は`miniflare`や`workerd`を使うことも考えたけど、結局それだとローカル完結になるし、REST APIだとシグネチャ通りに使えないのでやめた。
CLIではなくモジュールとしての`wrangler.unstable_dev()`はワンチャンあるかなと思ったけど、↑で書いたポイントが損なわれるし、そもそもunstableでうまく動かなかった。
個人的な需要にはドンピシャのライブラリなので、コレ相当のことが公式ツールセットでできるようになったらいいな〜(中の人見てる〜?)って思いながら、ほそぼそと育てていきたい気持ち。