Open6

Cloudflare Workers と Client Side Rendering

codehexcodehex

ここでの前提は Cloudflare Workers であり、Cloudflare Pages ではない。どうしても既存の Cloudflare Workers を活かしつつ、複雑なフロントエンドページを作成する必要が出てきた時のためにアイディアを残す。

できることなら Cloudflare Pages の利用をお勧めします。

codehexcodehex

pnpm を使った workspace モノレポ

リポジトリルートから見たツリー

 .
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── projects
    ├── frontend
    └── api

frontend

frontend project は Vite を利用している。cd projects && npx create vite で作成した React + TypeScript で初期化した。

vite.config.ts のみ修正

vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
	plugins: [react()],
	build: {
		outDir: "../api/assets/public",
	},
});

リポジトリルートから pnpm -F frontend run viterm -rf projects/api/assets/public && pnpm -F frontend run vite build を実行してバンドルされた静的ファイルを配信する。

api

api はもちろん Cloudflare Workers である。静的ファイルを Workers Site の機能を使って配信する。

wrangler.toml
[site]
bucket = "./assets"

開発環境

Hono を使う。

フロントエンド側のファイルが更新されたら Vite の HMR を使って開発サーバーがリロードされるように少し工夫する。

フロントエンドのコードがビルドされるとプレビューも更新されるように HTML を編集する。
このコードは Vite の Backend Integration を参考にした。

ローカル環境では .dev.vars を用意して VITE_SERVER=http://localhost:5173 のように変数を追加する。本番では VITE_SERVER に値を追加しない。

type Env = {
  VITE_SERVER?: string;
};

const app = new Hono<{ Bindings: Env; }>()
app.get('/public/*', serveStatic({ root: './' }));
app.get(
    '/',
    jsxRenderer(
      ({ children, viteServer }) => {
        const clientScript = ViteReactScriptTags({ viteServer });
        return (
          <html>
            <head>{clientScript}</head>
            <body>{children}</body>
          </html>
        );
      },
      { docType: true },
    ),
    async (c) => {
      return c.render(
        <>
          <div id="root"></div>
          <script
            type="module"
            src={`${c.env.VITE_SERVER}/src/main.tsx`}
          ></script>
        </>,
        {
          viteServer: c.env.VITE_SERVER,
        },
      );
    },
  );

const ViteReactScriptTags: FC<{ viteServer?: string }> = ({ viteServer }) => {
  if (viteServer === undefined) {
    return html``;
  }
  return html`<script type="module" src="${viteServer}/@vite/client"></script>
    <script type="module">
      import RefreshRuntime from '${viteServer}/@react-refresh';
      RefreshRuntime.injectIntoGlobalHook(window);
      window.$RefreshReg$ = () => {};
      window.$RefreshSig$ = () => (type) => type;
      window.__vite_plugin_react_preamble_installed__ = true;
    </script>`;
};

これでは不十分で、assets から manifest.json を読み取ってその内容に応じて script タグや style タグを動的に追加する必要がある。本番では VITE_SERVER に値を追加しないので、VITE_SERVER === undefined の時にこの動作を行うコードを追加する必要がある。

(割愛)

yusukebeyusukebe

これ開発サーバーはNode.js?それともworkerd?

Node.jsでいいのならば、@hono/vite-dev-serverにWorkersの環境つくるだけでいいかと思いました(今それがし易いようにリファクタしてます!)。

codehexcodehex

frontend は nodejs で vite で api は wrangler dev を使っていました。

codehexcodehex

全然関係ないけど、 Workers Site は KV に静的ファイルが保存されるので、バンドルサイズに影響はない。
こんな感じ