🌤️

remix上でcloudflare workersのdurable objectを使う

2022/05/19に公開

概要

remixアプリケーションをcloudflare workersのmodule workerとして実装し、
remixからdurable objectsを使ってみました。
この記事では簡単なカウンターの実装例を紹介します。

ソースコードと実行方法

remix-durable-object-counterをチェックアウトして、以下のコマンドを実行して下さい。
ローカルでremixが起動します。

npm install
npm run dev

実装の簡単な説明

durable objectsを使うremixアプリケーションの実装

module workerを実装してdurable objectsを使うように設定すると、remixのload関数やaction関数が実行される際に引数としてdurable objectsを操作するオブジェクトが渡されます。

カウンターをインクリメントする実装は以下のようになります。

app/routes/index.tsx

import { json } from "@remix-run/cloudflare";
import type {
  LoaderFunction,
  DataFunctionArgs,
  ActionFunction,
} from "@remix-run/cloudflare";
import { Form, useActionData, useLoaderData } from "@remix-run/react";

export const loader: LoaderFunction = async ({
  request,
  context,
}: DataFunctionArgs) => {
  const url = new URL(request.url);

  const counter = context.COUNTER as DurableObjectNamespace;
  const id = counter.idFromName("A");
  const obj = counter.get(id);
  const resp = await obj.fetch(`${url.origin}/current`);
  const count = await resp.text();

  return json(count);
};

export const action: ActionFunction = async ({
  request,
  context,
}: DataFunctionArgs) => {
  const url = new URL(request.url);

  const counter = context.COUNTER as DurableObjectNamespace;
  const id = counter.idFromName("A");
  const obj = counter.get(id);
  const resp = await obj.fetch(`${url.origin}/increment`);
  const count = await resp.text();

  return count;
};

export default function Index() {
  const count = useLoaderData();
  const actionData = useActionData();

  return (
    <div>
      <h1>Welcome to Remix on cloudflare workers!</h1>
      <div>count: {count}</div>
      {actionData && <div>action result: {actionData}</div>}

      <Form method="post">
        <button type="submit">increment</button>
      </Form>
    </div>
  );
}

module worker実装

durable objectsを使うために、remixアプリケーションを以下のようなmodule workerとして実装します。

worker/index.ts

import { createFetch } from "remix-cloudflare-workers-fetch";
import * as build from "../build";
//@ts-ignore
import assetJson from "__STATIC_CONTENT_MANIFEST";
import type { ServerBuild } from "remix-cloudflare-workers-fetch";
import { Counter } from "./counter";

const fetch = createFetch({
  build: build as unknown as ServerBuild,
  assetJson,
  mode: "production",
  options: {
    cacheControl: {
      bypassCache: true,
    },
  },
});

export default {
  fetch,
};

export { Counter };

durable objectsとしてのCounterクラスの実装自体は公式ドキュメントのカウンター実装例をほぼそのまま使っています。
remixから使うための改修は不要です。

remixアプリケーションは以下のようにimportします。

import * as build from "../build";

こちらのimportはremixの静的ファイルをworkers sitesを使って配布するためのものです。

//@ts-ignore
import assetJson from "__STATIC_CONTENT_MANIFEST";

workers sitesについては公式ドキュメントを参照して下さい。

remixのビルド

remixアプリケーションとmodule workerを別々にビルドするために、remix.config.jsからserverプロパティを削除します。

remix.config.js

/**
 * @type {import('@remix-run/dev').AppConfig}
 */
module.exports = {
  serverBuildTarget: "cloudflare-workers",
  // server: "./server.js",
  devServerBroadcastDelay: 1000,
  ignoredRouteFiles: ["**/.*"],
  // appDirectory: "app",
  // assetsBuildDirectory: "public/build",
  // serverBuildPath: "build/index.js",
  // publicPath: "/build/",
};

この設定にして以下のコマンドを実行するとサーバーサイドのモジュールが/buildに出力されます。

npx remix build

remix buildで作成したモジュールはworker/index.tsでimportしています。

module workerのビルド

サンプルコードではworkerをworkerディレクトリに実装しています。
workerのビルドはesbuildで行います。
ビルドの設定は以下の通りです。

build-worker.ts

import { build } from "esbuild";

build({
  entryPoints: ["./worker/index.ts"],
  bundle: true,
  sourcemap: true,
  format: "esm",
  outfile: "dist/index.mjs",
  minify: true,
  external: ["__STATIC_CONTENT_MANIFEST"],
}).catch(() => process.exit(1));

以下のコマンドでビルドを実行します。

npx ts-node build-worker.ts

ビルドが成功するとdist/index.mjsが作成されます。

workerの実行

wrangler.tomlを以下のように設定します。

name = "remix-durable-object-counter"
main = "dist/index.mjs"
workers_dev = true
compatibility_date = "2022-05-18"

[site]
bucket = "./public"

[durable_objects]
bindings = [{ name = "COUNTER", class_name = "Counter" }]

以下のコマンドで実行します。

npx wrangler dev

以上

Discussion