Closed7

Remix+CloudflareでWebサイトを作る 1(セットアップ・D1バインディング・UIライブラリ選定・Drizzle導入)

saneatsusaneatsu

【2024-02-01】作り始める

モチベーション

Remixもっと使えるようになりたい。
Cloudflare D1とかいうナウいやつ使ってみたい。
あとで見返すこともあるかと思うので、雑にスクラップに開発のログを残していく。

まずは丸パクリから...

Remix、v1チョットワカル
Cloudflare、ワカラナイ

という状態からスタート。

https://zenn.dev/necocoa/articles/remix-v2-with-cloudflare-pages-d1

このページを参考にした。
めっちゃ最小限でシンプルでわかりやすくて助かりました🙏🏻
ここまではほぼ丸コピ。

GithubActionsでmainブランチにpushした時に自動デプロイする

GithubActions、チョットワカル

https://zenn.dev/nwtgck/articles/1fdee0e84e5808

ここを参考にしてみたが、これはCloudflare Pagesへのデプロイ。
これを参考にするとデプロイに成功はするがURLにアクセスした際に何も表示されない。

色々調べたところCloudflare Wranglerとしてデプロイする必要がある。
D1使ってるとそうなるのか。

よしよし無事にデプロイできたぞ。

次はこっちに書いてあるように、Commit StatusのDetailsをクリックした時にお手軽にプレビューにアクセスするようにしたい。

ということで完成したのが以下。

※追記:以下のコードはデプロイを2回行ってしまっていて良くないです。こちらのコードが修正版です。

.github/workflows/cloudflare.yml
name: Deploy to Cloudflare

on:
  push:
    branches:
      - main

jobs:
  publish:
    runs-on: ubuntu-20.04
    permissions:
      contents: read
      deployments: write
      statuses: write

    steps:
      # ------------------------------------------------
      # デプロイ
      # ------------------------------------------------
      - name: 🔀 Checkout
        uses: actions/checkout@v3

      - name: ⏬ Install
        run: npm install
        working-directory: ./

      - name: 🔨 Build
        run: npm run build
        working-directory: ./

      - name: 🚀 Deploy # この段階でデプロイはされている
        uses: cloudflare/wrangler-action@v3
        with:
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: pages deploy ./public --project-name=YOUR_PROJECT_NAME

      # ------------------------------------------------
      # Github ActionsのCommit Statusの"Details"をクリック時に、デプロイされたページにアクセスする
      # ------------------------------------------------
      - name: 🌐 Publish to Cloudflare Pages to get url # idを設定してデプロイしたCloudflare PagesのUrlを取得するためだけに使用している
        id: cloudflare_pages_deploy
        uses: cloudflare/pages-action@v1
        with:
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          projectName: YOUR_PROJECT_NAME
          directory: ./public
          gitHubToken: ${{ secrets.GITHUB_TOKEN }}

      - name: 🔗 Add publish URL as commit status
        uses: actions/github-script@v6
        with:
          script: |
            const sha = context.payload.pull_request?.head.sha ?? context.sha;
            await github.rest.repos.createCommitStatus({
              owner: context.repo.owner,
              repo: context.repo.repo,
              context: "Cloudflare Pages",
              description: "Cloudflare Pages deployment",
              state: "success",
              sha,
              target_url: "${{ steps.cloudflare_pages_deploy.outputs.url }}",
            });

こういうデプロイ処理は早めに片しちゃいたいよなぁ。
Cloudflare、安いし簡単だし好きになっちゃう。

saneatsusaneatsu

【2024-02-02】エラー: MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start. There is likely additional logging output above.

エラー内容

SQL書いてDBのシードデータ投入周りを書いて色々いじってたら突然開発環境で http://0.0.0.0:8788/ にアクセスできなくなった。エラー内容は以下。

$ npm run dev

> dev
> remix dev --manual -c "npm run start"[ERROR] MiniflareCoreError [ERR_RUNTIME_FAILURE]: The Workers runtime failed to start. There is likely additional logging output above.

解決

Remix v2へアップデートした時のメモ | Web Scratch を参考にしてみると以下のような記述があった。

実際にプロセスを見てみると、すでに止まってるはずの workerd や node が残ってることがあるので、これを止めると解決した。

$ kill $(lsof -t -i:8788)

killして再度立ち上げ直したら問題なく画面が表示された。

saneatsusaneatsu

【2024-02-03】DBにシードデータを入れたい

D1のチュートリアル

デプロイ周り整えるのと同じくシードデータ追加とかは早めにやっておきたい。
前に(去年の6月頃)Remixの1系使ってたときは、Prisma+Typescriptでシードデータドーンできるのでわかりやすかった気がするんだけど何もわからない。
SQLiteも使ったことがない。とりあえずチュートリアルやろう。

Get started · Cloudflare D1 docs

シードデータを入れてDBのデータを取得してみる

wrangler コマンドの使い方いろいろ分かった。
チュートリアルにならってinit.sql を作成して以下をpackage.json に追加してREADMEも更新。
本当はPrismaのときのようにFaker使って簡単にダミーデータ作りたいけどやり方わからないので悲しみのSQL文直書き対応。

package.json
{
  "scripts": {
    ...
    "db:dev:init": "wrangler d1 execute YOUR_DB_NAME --local --file=./db/init.sql",
  }
}

TypescriptファイルにSQLを書いてユーザーを一覧取得するようなものも作ってみた。

app/models/user.server.ts
interface Env {
  DB: D1Database;
}

type User = {
  id: number;
  email: string;
};

export async function getUsers(env: Env): Promise<User[]> {
  try {
    const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();
    return results;
  } catch (e) {
    throw new Error("エラーが発生しました");
  }
}

一覧を取得して表示できるまでいった

saneatsusaneatsu

【2024-02-03】DBに接続しているコードがあるとブラウザアクセス時に Application Error が出てしまう

問題

ローカルからデプロイした際に、正常にデプロイで来ても、DBに接続しているコードがあるとブラウザでアクセス時に「Application Error」という画面が出てきてしまう

原因・解決方法

Cloudflareでデプロイする際にはプロダクションとプレビュー(=Staging)と2つの環境が存在する。

以下の記事を参考にした際には例として「プロダクション」のBindingのみを行っていた。
https://zenn.dev/necocoa/articles/remix-v2-with-cloudflare-pages-d1#binding-d1

ということで、原因はD1のBindingを「プロダクション」の方しかしていなかったからでした。
「プレビュー」タブでもD1のBindingを行うことで無事解決。

変更箇所に問題あると思ってなるべく小さく検証してたら時間食ってこれに1h弱かかってしまった...。

saneatsusaneatsu

【2024-02-03】Github Actionsを修正する

2回デプロイされてる

今気付いたんだけど、この書き方だとcloudflare/wrangler-action@v3cloudflare/pages-action@v1 で2回デプロイしてるから、Cloudflareのログを見ると当然2回分存在する。

ログが不要に溜まっていくしやめたいので cloudflare/pages-action@v1 の方は削除する。

解決

そのためにはcloudflare/wrangler-action@v3 の方だけでデプロイ先のURLを取得できれば良い。
公式ドキュメントを参考に以下のように修正して解決。

cloudflare/wrangler-action: 🧙‍♀️ easily deploy cloudflare workers applications using wrangler and github actions

.github/workflows/cloudflare.yml
      - name: 🚀 Deploy
        id: cloudflare-wrangler # ここを追加
        uses: cloudflare/wrangler-action@v3
        with:
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: pages deploy ./public --project-name=YOUR_PROJECT_NAME
      - name: 🔗 Add publish URL as commit status
        uses: actions/github-script@v6
        with:
          script: |
            const sha = context.payload.pull_request?.head.sha ?? context.sha;
            await github.rest.repos.createCommitStatus({
              owner: context.repo.owner,
              repo: context.repo.repo,
              context: "Cloudflare Pages",
              description: "Access Cloudflare Pages deployment",
              state: "success",
              sha,
              target_url: "${{ steps.cloudflare-wrangler.outputs.deployment-url }}", # ここを更新
            });
saneatsusaneatsu

【2024-02-03】Hello, MUI!

選定

前にRemix v1をいじっていたときはshadcn(Tailwind)使っていたけど、Exampleが充実しているとは言えないし、Comboboxとか以下の画像のようなUIを作るとなると一苦労。
そしてこのようなUIは今回使いたいと思っている。

UIライブラリを選定する時にAutocompleteがあると全体的に色々充実しているライブラリだなぁと思う。
ということで色々と充実しているMUIを使ってみる。

MUI CoreとMUI X、MUI Core内のパッケージの違いは以下の記事が参考になった。

https://zenn.dev/a_da_chi/articles/8fe30435792108#パッケージ構成からみるmuiの概要

以下の記事では、MUIの歴史的経緯や特徴が解説されてて参考になった。

https://zenn.dev/yumemi_inc/articles/2021-09-26-material-ui-v5#パフォーマンス

どれを使う

Material UI、Joy UI、Base UI、MUI Systemはどれを使えばよいのか?
結論、Joy UIを選択することにした。

Material UIがそもそもあまり好きじゃなくてもうちょっとモダンなやつが良いと思っていたが、どうやらJoy UIが該当しそう。
独自にThemeを反映させたり sx を使ってのカスタマイズもしやすそうだしこれでいいかな。
ただし、sxは他と比べて倍以上速度が遅いようなので多用しないほうが良さそう。

最悪部分的にMaterial UIでいいや。

Tailwindの設定を諸々消した。
MUI(Joy UI)で頑張っていこう。

saneatsusaneatsu

【2024-02-03】Hello, drizzle!

そういえば

今のところ公式ドキュメントにある通りに直接SQL文書いているけどPrisma的なORMどうしよう?

色々調べてみたところdrizzleかsqlcの2択っぽい。

https://zenn.dev/mizchi/articles/d1-drizzle-orm
https://zenn.dev/mizchi/articles/fullstack-remix-d1-boilerplate

どっちも使ったこと無いし特にこだわり無いけどdrizzle使ってみる。
これでfakerとか使ってtsファイルでシードデータ生成したり、スキーマも色々書けるようになる感じなのかな?

書いてみる

スキーマファイルを定義

db/schema.ts
/*
  DO NOT RENAME THIS FILE FOR DRIZZLE-ORM TO WORK
*/
import { sql } from "drizzle-orm";
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

const baseSchema = {
  id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
  createdAt: text("createdAt")
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
  updatedAt: text("updatedAt")
    .notNull()
    .default(sql`CURRENT_TIMESTAMP`),
};

export const usersSchema = sqliteTable("users", {
  ...baseSchema,
  email: text("email").notNull(),
});

以下のような書き方でusersの情報を取得。いい感じ。

app/routes/_index.tsx
import { usersSchema } from "db/schema";

export async function loader({ context }: LoaderFunctionArgs) {
  const env = context.env as Env;

  const users = await client(env.DB).select().from(usersSchema).all();
  // Before
  // const { results } = await env.DB.prepare("SELECT * FROM users").all<User>();

  return json({
    users: users ?? [],
  });
}

わからなかったこと

drizzleを使ってシードファイルを入れるTypescriptファイル書きたかったけど、やり方がわからなかった。
より具体的にはRemixのloader action で取れるcontextcontext.env.DBloader action を使用せずに取得する方法がわからなかった。
開発段階では1つのテーブルに3つくらいデータが入っていればいいから、一旦SQL直書きでいっかな...。

その他

そういやこの時で、プレビューとプロダクションのD1データベースが同じところを指すようにしたけど、プレビューをStgとして使用する、みたいな場合DBは2つ作って置くべきだった。

ちなみに、ここの記事は10ヶ月くらい前だけど、α版でのDBの上限数は10個らしい。

このスクラップは2ヶ月前にクローズされました