🍃このブログは移転しました。
3秒後、自動的に移動します・・・。

ローカルでのフロントエンド開発時でも、実際のCloudflareスタックにアクセスする

端的にいうと、

  • フロントエンドは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`なオプションも現状は存在しない。

https://github.com/cloudflare/workers-sdk/blob/528cc0fc583e9672247d5934c8b33afebbb834e7/packages/wrangler/src/pages/dev.ts#L53

(そしてこのコマンドの場合、`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はまだ公開されてなさそうではあるが、内部的には存在する気配を感じるので、そのうちできるようになりそう。

https://github.com/cloudflare/workers-sdk/blob/436f752d77b12b81d91341185fc9229f25571a69/packages/wrangler/src/d1/execute.tsx#L342

がんばってモックするパターンは、KVのREADだけあればいいとか、接点が小さいならばありかもしれんけど、エッジに生きるものとしてはやっぱ使い倒したいよな〜って。

他のメタフレームワーク

SvelteKitでなくても、Viteベースのメタフレームワークを使う場合、おそらく同じ結果になるはず。

  • SolidStart
  • Astro
  • QwikCity

少なくともこのあたりは。

Next.jsは、そもそも`@cloudflare/next-on-pages`っていうそれ用のレイヤーがないとまともに立てもしない状態な上、READMEを見る限り`wrangler pages dev`を使うらしいので、実データは参照できなそう。

https://github.com/cloudflare/next-on-pages/blob/b1c3a3389b64d6370136d3bff08991edc9de43c1/packages/next-on-pages/README.md?plain=1#L76

Remixは、たしかViteベースではなかったはずやけど、最新のテンプレを見る限り`wrangler pages dev`を使うようだった。

https://github.com/remix-run/remix/blob/3f0ba3780c748782c31df2d19392909765a28f5d/templates/cloudflare-pages/package.json#L7

コード更新からの反映速度が気にならず、ローカル完結でよければ、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スタックにアクセスしたい

場合は、モックの実装を自分で差し込むしかなさそう。

ならば、せめて、そのモックを楽に使えるようにしようと作ったのがコレ。

GitHub - leader22/cfw-bindings-wrangler-bridge: Bridge between local development code and Cloudflare bindings, via `wrangler dev --remote` command.

簡単にいうと、

  • 予めローカルで起動しておいた`wrangler dev --remote`のサーバーに
  • APIと同じシグネチャのモック実装からアクセスできる

というもの。

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`自体がアップデートされても、それは使う側が選べる
  • アプリ内のコードが汚れない
    • ローカルなURLだけ管理すればいい

というあたりがポイントかも。

最初は`miniflare`や`workerd`を使うことも考えたけど、結局それだとローカル完結になるし、REST APIだとシグネチャ通りに使えないのでやめた。

CLIではなくモジュールとしての`wrangler.unstable_dev()`はワンチャンあるかなと思ったけど、↑で書いたポイントが損なわれるし、そもそもunstableでうまく動かなかった。

個人的な需要にはドンピシャのライブラリなので、コレ相当のことが公式ツールセットでできるようになったらいいな〜(中の人見てる〜?)って思いながら、ほそぼそと育てていきたい気持ち。