☘️

Supabase+Remix+Cloudflare WorkersでHello, World

2022/02/07に公開

GitHub

ソースを上げました。
https://github.com/smallStall/helloSRC

今回の目標

表題にもありますが、3つのシステムを組み合わせてHello, Worldするまでやってみたいと思います。具体的にはSQLのSELECTでHello, WorldというデータをSupabaseから取ってきて表示するところまでですね。それぞれの役割をざっくり表すと以下の通りです。

  • Supabae データベース担当
  • Remix ビュー・コントローラー担当
  • Cloudflare Workers サーバー担当

こうして見るとRemixだけ担当範囲が広いような気がします。
事前の注意ですが、3つとも活発に更新するためバージョンが合わないとうまく動かない可能性が高いです。人間でたとえるなら、三角関係ってことですね。なので、試してみたい方がいらっしゃいましたらバージョンにご注意ください。また、同様の理由でこの記事の賞味期限が短いことにもご留意ください。
RemixとWorkersを動かすところから始めます。それでは行ってみましょう。

RemixとWorkersの環境構築

Remixが公式で用意されています。
https://www.npmjs.com/package/remix
以下の通り実行すればインストールできます。ただ、Supabaseとの相性を考えるとリポジトリをgit cloneするのをおすすめします。Remix@1.2.1では後述のcross-fetchからWeb API Fetchへの入れ替えでエラーが出ました。このリポジトリにはRemixとSupabaseとCloudflare Workersのバージョンが記載されていますが、Remix部分の中身がないので1から作る必要があります。

公式の方法
npx create-remix@latest
#Cloudflare Workersを選択
#Typescriptを選択
#npm install?をYes
おすすめの方法
git clone https://github.com/smallStall/RSCStarter
cd RSCStarter
npm install

https://github.com/smallStall/RSCStarter
git cloneしたら、Cloudflare Workersの設定ファイルwrangler.tomlがセキュリティの関係上抜けているので、ルートに作成します。

wrangler.toml
name = "remix-supabase-cloudflare-workers-starter"
type = "javascript"

zone_id = ""
account_id = ""
route = ""
workers_dev = true

[site]
bucket = "./public"
entry-point = "."

[build]
command = "npm run build:worker"
watch_dir = "build/index.js"

[build.upload]
format="service-worker"

Remix+WorkersでHello, World

次にHello, Worldを書きます。以下の通り、ファイルを作ります。

app/root.tsx
import { useLoaderData, Scripts } from "remix";
import type { LoaderFunction } from "remix";

export const loader: LoaderFunction = () => {
  return { message: "Hello, Remix!" };
};

export default function Index() {
  const data = useLoaderData();
  return (
    <html lang="jp">
      <body>
        <h1>{data.message}</h1>
        <Scripts />
      </body>
    </html>
  );
}

実行は以下の通りにすればstartでブラウザのタブが開きます。

npm run dev
#ターミナルをもう1つ開く
npm start

これでHello, Remix!と表示されればOKです。このコードの詳細は以下のページに書きました。
https://zenn.dev/smallstall/articles/bd8272fdde5038

Supabaseのテーブル作成

公式のHPに行ってsign upし、projectを作成します。詳細は省いちゃいます。
https://supabase.com/

さて、Supabaseのprojectを作成できたので、テーブルを作ります。
table editorを開きます。

「Create a new table」ボタンを押し、出てきたパネルに入力します。

この状態ではセキュリティがまずいんですが、重要なデータを入れるわけではないので、今は良しとします。
「Save」ボタンを押せば、テーブルが作成されます。
作成されたら、「Insert row」ボタンを押して下記の通りデータを入力しました。

saveしてもう一行入れておきます。

合計で2行入りました。
次にSupabaseのSite URLのところにlocalhostを登録します。Authenticationアイコン→Settingsタブを選択します。


Site URLにlocalhostを入力します。

これでSupabase側の準備は完了です。

RemixからSupabaseに接続する

ここがけっこう難しく感じました。がんばります。
データベースに接続するにはSupabaseのcreateClient関数を実行します。
https://github.com/supabase/supabase-js#usage
これをRemixのloader内で行います。全体の流れはそんな感じです。
createClientの引数はsupabaseURL、supabaseKey、optionです。supabaseURLとsupabaseKeyについては、SupabaseのSettingsアイコン→APIタブの順に選択すると表示されます。



supabaseURLとsupabaseURLを環境変数に設定します。まず開発環境の環境変数を設定します。

npm i dotenv

https://github.com/motdotla/dotenv
ルートに.envファイルを作成し、supabaseURLとsupabaseKeyをコピペします。

.env
SUPABASE_URL=supabaseURLをコピペ
SUPABASE_ANON_KEY=supabaseKeyをコピペ

https://remix.run/docs/en/v1/guides/envvars#environment-variables

.envの中身が公開されると困るので.gitignoreに加えてあります。

.gitignore
/.env

型を定義します。app配下にファイルを作成します。

app/bindings.ts
export {};
declare global {
  const SUPABASE_ANON_KEY: string;
  const SUPABASE_URL: string;
}

これで開発環境において環境変数が使えるようになりました。
rootを次の通り書き換えます。

app/root.tsx
import { useLoaderData } from "remix";
import type { LoaderFunction } from "remix";
import { createClient } from "@supabase/supabase-js";

export const loader: LoaderFunction = async () => {
  const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
    fetch: (...args) => fetch(...args),
  });
  return { message: "Hello, Remix!" };
};

export default function Index() {
  const data = useLoaderData();
  return <h1>{data.message}</h1>;
}

この状態ではまだcreateClientを実行しただけですので、データベースにアクセスする準備しかしていません。createClientのoptionにあるfetchのところに注目してみます。何やら呪文のようなものが書かれていますね。

app/root.tsx
    fetch: (...args) => fetch(...args),

ここではSupabaseのfetch(cross-fetch, Node.jsベースのfetch)をRemixのfetch(Web Fetch API)に書きかえています。
https://github.com/supabase/supabase-js#custom-fetch-implementation
より詳しくはこちらのIssueで議論されていました。fetchを置き換える最も汎用性が高い書き方のようです。
https://github.com/supabase/postgrest-js/pull/235
postgrest-jsというのはsupabase-jsのうちpostgrestに関わる部分です。supabase-jsは色々なものをまとめているので、postgrest-jsの方が情報が早い場合がありそうです。
https://supabase.com/docs/reference/javascript/supabase-client
Cloudflare WorkersにおいてもWeb Fetch APIが使用されています。
https://developers.cloudflare.com/workers/runtime-apis/fetch
これらのことを踏まえて、root.tsxにコードを付け加えます。

app/root.tsx
import { useLoaderData, Scripts } from "remix";
import type { LoaderFunction } from "remix";
import { createClient } from "@supabase/supabase-js";

type Message = {
  title: string;
};

export const loader: LoaderFunction = async () => {
  const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
    fetch: (...args) => fetch(...args),
  });
  const { data } = await supabase.from<Message>("message").select("title");
  return data;
};

export default function Index() {
  const messages = useLoaderData<Message[]>();
  return (
    <html lang="jp">
      <body>
        <div>
          {messages.map((message) => (
            <h1>{message.title}</h1>
          ))}
          <Scripts />
        </div>
      </body>
    </html>
  );
}

本当はtypeやcreateClientは別ファイルにした方がコードの見通しが良いのですが、まずは動かすのを最優先にしました。
これでHello, Supabase, Remix, and Workersと表示されていればRemix経由でデータベースから引っ張ってきたことになります。エラーはないけどうまく表示されない場合、ブラウザのキャッシュが効いている可能性があります。スーパーリロードを試せば更新されるかもしれません。
selectは何を返していのかが気になります。公式を見てみると、{data, error}を返すようですね。
https://supabase.com/docs/reference/javascript/select
このdataというのは何を指しているんでしょうか?Supabaseのコードをざっくり覗いてみますと、PostgrestResponseSuccessインターフェイスのdataのようです。

types.ts
interface PostgrestResponseSuccess<T> extends PostgrestResponseBase {
  error: null
  data: T[]
  body: T[]
  count: number | null
}

https://github.com/supabase/postgrest-js/blob/master/src/lib/types.ts
https://supabase.github.io/postgrest-js/classes/postgrestfilterbuilder.html
なので、今はMessage型の配列ですね。loaderの戻り値がプレーンなオブジェクトな場合は自動的にResponseになる旨、Remixに記載がありました。
https://remix.run/docs/en/v1/api/conventions#returning-response-instances
オブジェクトの配列の場合も同じように配列のResponseになるのでしょうか? 挙動を見る限りはそのようですが、わかりません。また今度調べることにします。
話をroot.tsxに戻します。useLoaderDataによりResponseを受け取り、Message型の配列が帰ってきます。配列をmapでh1タグに展開しています。
若干よくわからないところもありますが、大まかな流れはそんな感じのようです。

Cloudflare Workersにデプロイ

CloudflareにSign upします。詳細は割愛します。
https://workers.cloudflare.com/
Cloudflareのダッシュボードが開いたら、次にWorkersのCLIをダウンロードします。

npm install -g @cloudflare/wrangler

https://developers.cloudflare.com/workers/#installing-the-workers-cli
wranglerコマンドでログインします。

wrangler login
Allow Wrangler to open a page in your browser? y

https://developers.cloudflare.com/workers/get-started/guide#3-configure-the-workers-cli
「You have granted authorization to Wrangler!」と表示されればユーザー承認されました。
デプロイする前に本番環境でも環境変数が使えるようにします。ターミナルを開いて環境変数を入力します。

wrangler secret put SUPABASE_URL
#supabaseURLを入力
wrangler secret put SUPABASE_ANON_KEY
#supabaseKEYを入力

wranglerの設定ファイル、wrangler.tomlに必要事項を入力します。入力するのは機密情報なので.gitignoreに入れてあります。

.gitignore
/wrangler.toml

ターミナルで以下を実行します。

wrangler whoami

これでアカウント名とアカウントIDが表示されます。
https://developers.cloudflare.com/workers/get-started/guide#7-configure-your-project-for-deployment
アカウントIDをwrangler.tomlにコピペします。

wrangler.toml
zone_id = ""
account_id = "ここにコピペ"
route = ""

これで役者は揃ったのでデプロイしましょう。publishというコマンドです。ちょっとオシャレですね。

wrangler publish

https://developers.cloudflare.com/workers/get-started/guide#8-publish-your-project
うまく行けば、ターミナルに https:なんちゃらworkers.devと表示されます。これをクリックしてみましょう。
さらにうまく行けば、Hello, なんちゃらが表示されます。これでサイトがデプロイされました。素晴らしい!

次回

今回はとりあえず動かすのを最優先にしたため、サイトとしての機能はほとんどありませんでした。次回からはCRUDや認証周りをやってみようと思います。書きました。
https://zenn.dev/smallstall/articles/631342c94dfc44
現場からは以上です。

Discussion