👻

React Router + Hono + Cloudflare のテンプレートを作る

に公開

React Router のテンプレート

React Router では Cloudflare 用の公式のテンプレートが用意されています。しかし Hono を含んだテンプレートがないため、今回、用意しました。

https://github.com/SoraKumo001/react-router-templates

使い方

使い方は基本的に公式のテンプレートと同じです。

npx create-react-router@latest --template sorakumo001/react-router-templates/cloudflare

公式のテンプレートとの違い

こちらが公式テンプレートです

https://github.com/remix-run/react-router-templates/tree/main/cloudflare

Middleware として Hono を導入

React Router の Middleware はまだ開発段階なので、現時点では Hono を使ったほうが無難です。

@cloudflare/vite-pluginを除去

@cloudflare/vite-pluginを使うと、開発時に Cloudflare 環境の再現性が高まるのですが、外部モジュールの初期インポートに時間がかかるので、依存モジュールが増えていくと辛くなっていきます。また、rolldown-viteとの併用ができないため、今回は取り除きました。

rolldown-viteの導入

rolldown-viteを使うと、ビルド速度が半分以下に短縮されます。圧倒的な速度です。

カスタマイズ部分の解説

vite.config.ts

Hono が使えるように設定しています。ただし Hono の Cloudflare 用の Adapter を直接使わず、処理を修正しています。理由として元の Adapter は navigator.userAgent の値を偽装する処理が入っているため、Prisma の queryCompiler の機能を使おうと思った場合に、環境の識別ができず正常に動作しなくなるためです。

また、開発実行時と Deploy 時で、起動コードを共通化するためにvirtual:react-router/server-buildをエイリアスで切り替えています。@cloudflare/vite-pluginを使った場合は、別の方法で起動コードの自動切り替えが行われるのですが今回未使用のため、この設定を入れています。

svg など import 系の Assets に関しては、利用するものを適宜指定する必要があります。足りないものは exclude で追加指定してください。これをやらないと、開発時に対象のファイルが表示されません。

import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import serverAdapter from "hono-react-router-adapter/vite";
import type { cloudflareAdapter } from "@hono/vite-dev-server/cloudflare";
import { getPlatformProxy } from "wrangler";
import { defaultOptions } from "@hono/vite-dev-server";

// Entry file
const entry = "./workers/app.ts";

// Prevent tampering with Hono's Cloudflare parameters executed by default
const adapter: typeof cloudflareAdapter = async (options) => {
  const proxy = await getPlatformProxy(options?.proxy);
  return {
    env: proxy.env,
    executionContext: proxy.ctx,
    onServerClose: () => proxy.dispose(),
  };
};

export default defineConfig({
  resolve: {
    alias: [
      {
        find: "../build/server/index.js",
        replacement: "virtual:react-router/server-build",
      },
    ],
  },
  ssr: {
    resolve: {
      externalConditions: ["worker"],
    },
  },
  plugins: [
    serverAdapter({
      adapter,
      entry,
      // Asset adjustment
      exclude: [...defaultOptions.exclude, /\.(webp|png|svg)(\?.*)?$/],
    }),
    tailwindcss(),
    reactRouter(),
    tsconfigPaths(),
  ],
  experimental: { enableNativePlugin: true },
});

workers/app.ts

起動コードです。Hono を使って、React Router を呼び出しています。前述した通り../build/server/index.jsは、Vite からはvirtual:react-router/server-buildに変換され、deploy などでwranglerから呼び出されるときは変換せずに使われます。

import { Hono } from "hono";
import { contextStorage } from "hono/context-storage";
import { getLoadContext } from "load-context";
import { createRequestHandler } from "react-router";

const app = new Hono();
app.use(contextStorage());

app.use(async (c) => {
  // @ts-ignore
  const build = await import("../build/server/index.js");
  // @ts-ignore
  const handler = createRequestHandler(build, import.meta.env?.MODE);

  const next = (input: Request | string, init?: RequestInit) => {
    return handler(new Request(input, init), {
      cloudflare: { env: c.env },
    });
  };
  const context = getLoadContext({
    request: c.req.raw,
    context: {
      cloudflare: {
        env: c.env,
        next,
      },
    } as never,
  });
  return handler(c.req.raw, context);
});

export default app;

.env

NODE_ENV を production に設定しています。これがなんの為にあるのかというと、wrangler deploy時に必要だからです。wranglerは、バックエンド側のコードを esbuild でビルドします。そして環境変数が設定されていないと node_module に含まれている react が、デバッグ用の開発パッケージをビルドに加えてしまい、性能の低下やサイズの増加を引き起こします。フロント側は影響を受けませんが、バックエンド側ではこの問題が発生します。

これ、結構重大な問題なのですが、巷のリポジトリを覗いてみても、だれも対処していません。

# If this configuration is missing during `wrangler deploy`, the React development module will be used.
NODE_ENV=production

まとめ

React RouterNext.jsと比べると、初期設定の選択肢が多いため、それがハードルになっている感があります。ただ、Cloudflare 用の React 用フレームワークと考えた場合、他に選択肢がありません。Next.jsopennextから使うという手を思いつくかもしれませんが、ビルドで吐き出されるサイズが巨大すぎてはっきり言って問題外です。

公式のテンプレートは@cloudflare/vite-pluginを使ったものになっており、本番に近い環境をエミュレーションして開発できる反面、モジュールの読み込みにクセがあります。依存パッケージによっては、開発が難しくなる場合があります。原理を話すと長くなるのですが、エミュレーション環境であるworkerdでモジュールのやりとりをするとき、ESM しか対応していないので、node_modules の魑魅魍魎なパッケージ群をうまく変換しながら渡す必要があります。ビルド時は全体を一括でまとめて変換できるのでよいのですが、開発時は部分的にビルドしながら渡していかねばならず、このときにモジュールの整合性をとる作業が破滅的に複雑です。そのため、@cloudflare/vite-pluginを使った場合、依存パッケージが増えるに従いどんどん動作が重くなっていきます。

ということで、あえて@cloudflare/vite-pluginを外したテンプレートを作りました。開発時の再現性は劣りますが、快適性は圧倒的に良くなっています。公式テンプレートで、依存パッケージの問題で苦労している人は使ってみてください。

GitHubで編集を提案

Discussion