カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

Remix + Cloudflare Pages + Cloudflare Workers KV を触ってみた話

こんにちは!カミナシでソフトウェアエンジニアをやっているくらさわです! 今回は、以前から触ってみたかった Remix + Cloudflare Pages + Cloudflare Workers KV で簡単に素振りしてみた話を書きたいと思います!

なぜ触ってみたかったのかというと、 エッジコンピューティングはよく聞くけど実際にちゃんと使ってみたことがなく、エッジで SSRできる Remix にも元々興味があったためです。

初めに触った技術に関して軽く説明していますが、本当に軽くなので気になる方は公式サイト等をご覧ください。

Remix について

Remix とは SSR が得意な React 向けの Web フレームワークです。 後述する Cloudflare Pages にデプロイして SSR することができます。

公式サイト: Remix - Build Better Websites

Cloudflare Pages について

Cloudflare Pagesとは Cloudflare が提供する Web サイトを作成、ホスティングするためのサービスです。 Cloudflare Workers の機能を利用して、サーバーレス関数を実行することもできます。

公式サイト: Cloudflare Pages

Cloudflare Workers KV について

Cloudflare Workers KV とは Cloudflareのアプリケーション向けサーバーレス Key-Value ストレージです。

公式サイト: サーバーレスストレージとアプリケーション | Cloudflare Workers KV | Cloudflare

開発について

それでは実際の開発について説明していこうと思います。

create-remix で作成したものに、Remix のチュートリアルの冒頭の部分を追加、編集したりして試してみました。

Blog Post を閲覧できるサイトです。

チュートリアル: Remix | Blog Tutorial (short)

ちなみにチュートリアルはシュッとできて、勉強になるのでおすすめです。

完成品: https://cloudflare-pages-remix-sample.pages.dev
github リポジトリ: GitHub - n-kurasawa/cloudflare-pages-remix-sample

Remix プロジェクトの作成

まずは、Remix のプロジェクトを作成します。

$ npx create-remix@latest

? Where would you like to create your app? cloudflare-pages-remix-sample
? What type of app do you want to create? Just the basics
? Where do you want to deploy? Choose Remix App Server if you're unsure; it's easy to change deployment targets. Cloudflare Pages
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes

create-remix を実行するとインタラクティブに質問してくれるので、それに答えていくことで簡単にプロジェクトの設定が完了します。

今回は特に、Cloudflare Pages にデプロイしたいため Cloudflare Pages を選択しています。

ここで作成したプロジェクトは GitHub に push しておきます。

Cloudflare Pages にデプロイ

Cloudflare にアカウントを作成して、ダッシュボードを開いて、Pages にプロジェクトを作成します。

GitHub ( GitLab も可能 ) に接続して、先ほど作成したプロジェクトのリポジトリを選択します。

最後にビルドとデプロイのセットアップをして、デプロイします。これ以降、リポジトリに push するたびにデプロイが走ります。

めっちゃ簡単です!

Cloudflare Workers KV の作成

ダッシュボードの KV から名前空間を作成します。

名前には大文字のスネークケースが使われてる例をよく見るので、それっぽく入れておきます。

バインディングの設定

作成した KV を Pages から使用するためにバインディングを設定します。 ここで設定した変数名をコード上では使用することになります。

Pathとルーティング

ここからはコードの説明に入っていきたいと思います。

Path は API を含めると全部で 4 つ作成しています。

Path 対応するファイル 内容
/ app/routes/index.tsx トップページ
/posts app/routes/posts/index.tsx Blog Post 一覧
/posts/$slug app/routes/posts/$slug.tsx Blog Post 詳細
/api/posts functions/api/posts/index.ts Blog Post 一覧取得API

このように Remix では、Path に対応するファイルを作成する、ファイルベースのルーティングを行っています。

API に関してはRemix ではなく Cloudflare Pages Functions の機能でルーティングされますが、こちらもファイルベースとなっています。

Remix | Routing
Functions の Routing: Routing · Cloudflare Pages docs

Remix の Loader について

以下は Blog Post 一覧の app/routes/posts/index.tsx の一部です。

export const loader = async ({ request }: LoaderArgs) => {
  const url = new URL(request.url)
  const res = await fetch(`${url.origin}/api/posts`)
  const posts: Post[] = await res.json()
  return json({ posts })
}

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>()
  return (
    <main>
      <h1>Posts</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link to={post.slug}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </main>
  )
}

コード: cloudflare-pages-remix-sample/index.tsx at main · n-kurasawa/cloudflare-pages-remix-sample · GitHub

上記のように loader という関数を作成し、外部からのデータ取得をそこで行います。

コンポーネント側では useLoaderData を使用して、取得したデータを使用することができます。

Remix | Data Loading

ちなみに loader 内で、ブラウザで動く fetch のノリで fetch('/api/posts') のように path だけ渡しても動かないので URL の形式で渡す必要がありました。

Cloudflare Pages Functions

上記の loader では Cloudflare Pages Functions で作成した API を叩いています。Functions とは Cloudflare Workers のようにサーバレス関数を定義できるものです。

以下は Blog Post 一覧取得API の functions/api/posts/index.ts の一部です。

export const onRequest: PagesFunction<Env> = async (context) => {
  const { keys } = await context.env.POSTS_KV.list()
  let posts: Post[] = []
  for (const key of keys) {
    const post: Post | null = await context.env.POSTS_KV.get(key.name, {
      type: 'json',
    })
    if (post) {
      posts.push(post)
    }
  }
  return new Response(JSON.stringify(posts))
}

コード: cloudflare-pages-remix-sample/index.tsx at main · n-kurasawa/cloudflare-pages-remix-sample · GitHub

Functions の onRequest はリクエストメソッドに関係なく、該当する path へのリクエスト全てに対して呼び出されます。

ここでは、 Cloudflare Workers KV に保存してある Post の一覧を取得しています。

Functions から KV には、引数の context.env から バインディングで設定した変数名でアクセスすることができます。 POSTS_KV というのが私が設定した変数名です。

API Reference · Cloudflare Pages docs
Bindings · Cloudflare Pages docs

Remix の Lodaer でのバインディング

またRemix では、 functions ディレクトリ配下にファイルを作成しなくても Remix の loader 関数から KV にアクセスすることができます。 以下は Blog Post 詳細 の app/routes/posts/$slug.tsx の一部です。

export const loader = async ({ context, params }: LoaderArgs) => {
  invariant(params.slug, `params.slug is required`)

  const kv = context.POSTS_KV as KVNamespace
  const post: Post | null = await kv.get(params.slug, { type: 'json' })
  invariant(post, `Post not found: ${params.slug}`)

  const html = marked(post.markdown)
  return json({ post, html })
}

コード: cloudflare-pages-remix-sample/$slug.tsx at main · n-kurasawa/cloudflare-pages-remix-sample · GitHub

loader で バインディングを利用する場合も、引数の context から取得します。ただし、Functions とは違って、env からではなく、context から直接利用する形になっています。

ちょっと型付けをどうするのがいいのかわからなかったので無理やり感がありますが。

Local 開発について

また KV を使用する場合でも、ローカルで開発できるようになっています。

以下のようにpackage.json に定義してある dev 用のコマンドに --kv {{ バインディング変数名 }} をつけるといいです。

"dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public --kv POSTS_KV",

コード: cloudflare-pages-remix-sample/package.json at main · n-kurasawa/cloudflare-pages-remix-sample · GitHub

そうすると local storage を使って KV の動作をシミュレートしてくれます。

ちなみに、データはデフォルトでは永続化されないので永続化したい場合は、--persist をつけます。

Local Development · Cloudflare Pages docs

まとめ

いかがでしたでしょうか? 簡単な説明しかしていませんが、なんとなく雰囲気が伝わっていれば幸いです。 個人的な感想としては、思ったより、Remix も Cloudflare Pages も使いやすくていいな〜と思いました!

今回触った技術は、業務では使っておらず週末の自由研究としてやっていましたが、まだまだ表面的な部分しか触れていないので実際に何か作ってみようと思います。

そんな自由研究が好きな方も、そうでもない方もカミナシでは絶賛採用中です! 少しでもご興味があればよろしくお願いします!

careers.kaminashi.jp