Open10

remixとhonoを使う

uretanouretano

環境作成

$ pnpm create cloudflare@latest

フレームワークに remix を使うため、以下の質問に対して回答する。


╭ Create an application with Cloudflare Step 1 of 3
│ 
├ In which directory do you want to create your application?
│ dir ./my-app
│
├ What would you like to start with?
│ category Framework Starter
│
├ Which development framework do you want to use?
│ framework Remix

versions

ライブラリ バージョン
pnpm 9.1.4
react 18.2.0
remix-run/react 2.10.3
@remix-run/cloudflare 2.10.3
hono 4.5.5
vite 5.1.0
uretanouretano

ルートにserver.ts を作成する

server.ts
import type { AppLoadContext } from "@remix-run/cloudflare";
import { createRequestHandler } from "@remix-run/cloudflare";
import { Hono } from "hono";

const app = new Hono();

app.get("/api/hello", (c) => {
  return c.json({ message: "🔥" });
});

app.all("*", async (c) => {
  try {
    // @ts-expect-error it's not typed
    // eslint-disable-next-line import/no-unresolved
    const build = await import("virtual:remix/server-build");
    const handler = createRequestHandler(build, "development");
    const remixContext = {
      cloudflare: {
        env: c.env,
      },
    } as unknown as AppLoadContext;
    return handler(c.req.raw, remixContext);
  } catch (error) {
    console.error(error);
    process.exit(1);
  }
});

export default app;

ついでに /api/hello にAPIエンドポイントを作成しておく。

uretanouretano

vite.config.ts を編集する

vite.config.ts
import devServer, { defaultOptions } from "@hono/vite-dev-server";
import adapter from "@hono/vite-dev-server/cloudflare";
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  ssr: {
    resolve: {
      externalConditions: ["workerd", "worker"],
    },
  },
  plugins: [
    remix(),
    devServer({
      adapter,
      entry: "server.ts",
      exclude: [...defaultOptions.exclude, "/assets/**", "/app/**"],
      injectClientScript: false,
    }),
  ],
});
uretanouretano

functions/[[path]].ts を編集する

functions/[[path]].ts
import { handle } from 'hono/cloudflare-pages'
import server from '../server'
export const onRequest = handle(server)
uretanouretano

開発サーバーを立て、http://localhost:5173 にブラウザでアクセスすると remix で作られたコンポーネントを表示する。
また、 http://localhost:5173/api/hello に curl コマンドでアクセスすると、server.ts で定義した通り{ message: "🔥" }という Json の値が返却される。

uretanouretano

loader内でfetchする

loader 関数内で fetch して コンポーネントから useLoaderData 経由で値を取得する。

app/routes/_index.tsx
export const loader = async () => {
  const response = await fetch("http://localhost:5173/api/hello");
  const data = (await response.json()) as { message: string };

  return json({ data });
};

export default function Index() {
  const { data } = useLoaderData<typeof loader>();

  return (
    <div className="font-sans p-4">
      <h1 className="text-3xl">Welcome to Remix on Cloudflare</h1>
      {data && <p>{data.message}</p>}
    </div>
  );
}
uretanouretano

develop環境とproduction環境でfetchするURLが異なるので、環境変数で管理する。
cloudflareではwrangler.tomlで環境変数を設定できる。

wrangler.toml
name = "my-app"
pages_build_output_dir = "build/client"

[vars]
API_HOST = "http://localhost:5173"

[env.production]
  [env.production.vars]
  API_HOST = "https://XXX.my-app-XXX.pages.dev"

wrangler.tomlを参照して環境変数の型を生成する

$ pnpm wrangler types
worker-configuration.d.ts
interface Env {
+  API_HOST: "http://localhost:5173";
}

loader関数内で環境変数を読み込みfetchする

app/routes/_index.tsx
export const loader = async ({ context }: LoaderFunctionArgs) => {
  const host = context.cloudflare.env.API_HOST;

  const response = await fetch(`${host}/api/hello`);
  const data = (await response.json()) as { message: string };
  return json({ data });
};
uretanouretano

デプロイする

コマンド一つでデプロイできる。

$ pnpm run deploy

pnpmを使う場合、もともと存在するpnpm deploy とかぶってしまうため、package.jsonのscriptsに記載したdeployを実行する場合には pnpm run deployのようにrunをつける必要がある。