Closed32

👌 Blitz.jsで古臭いBBSを作るやつのメモ #01

ピン留めされたアイテム

#02: https://zenn.dev/hanak1a/scraps/98e7cf2e25f708

一通りのCRUDが出来たので一旦〆

感想としてはこんな感じ

https://twitter.com/hanak1a_/status/1345048072343093250
https://twitter.com/hanak1a_/status/1345048302161518592

追記: 2021/01/02

  • APIのルートは自分で定義できるっぽいので、型付きでシュッと始められるAPIサーバーとしてはBlitzは結構有力そう
  • ただし、公式ドキュメントを斜め読みした感じ、ActiveRecordにおけるModelみたいな、「ドメインロジックを担保する層」がまだないので、Mutationsあたりの設計をどうするかというのがアプリ開発者の課題になりそう

インストールは500万のWebサイトに書いてある(大嘘)からまず公式ドキュメント通りやれよな

手癖でMySQLにつなごうとして schema.prisma と .env.local を触ってみたけどなんか繋がらんから放置!!! 本番だったらMySQLにしたいけどここは本番ではないのでSQLiteでやる。

initial-migrationにはUserとSessionのモデルが定義されている(今現在)
とりあえずEntryモデルを雑に生やす。 構文は定義済みモデルの見様見真似

model Entry {
  id         Int      @id @default(autoincrement())
  user_id    BigInt
  content    String
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt

  user User @relation(fields: [user_id])
}

ここまで書いて気づいたらUserモデルにrelationが勝手に生えてた…
お前、もしかして"文明"なのか…?? VSCodeにPrisma Extension入れたら保存時にフィールド名とか見てなんかよしなにやるっぽいな…

created_atが@default(now())なのが気になる。 これはBlitzかPrisma側が実行時に設定してくれるのか、それともDBのスキーマに設定されるのか…(後者だとAP/DBでタイムゾーンが異なったときに死ぬという問題に当たりそう)

ちょっと待って、schema.prisma2つあるけどどっちに設定したらええねん…

→ 落ち着いて公式ドキュメントを読みましょう。 これを書いてる時点ではdb/schema.prisma が正解っぽいですね

https://blitzjs.com/docs/database-overview

blitz db migrateしたらmigrations以下のやつが勝手に生成された……
文明だ……

初めてBlitz.jsでアプリを作りました! よろしくお願いします!

http://localhost:3000

はい通過儀礼。

Blitzくん、なんとユーザー登録とログインは最初から実装済み。
2021年ですね。

じゃあなんか雑にページを作っていきましょう。
サモン! app/pages/latest.tsx!!!!!
(間違えて app/auth/pagesにファイルをブチ込む音)

Pageは見たところ普通のNext.jsっぽいですね。
違いはComponent.getLayout とかいう見たことないメソッドが生えてるくらいかな。

getLayoutは_app.tsx内で呼び出されてるので、アプリ開発者が任意で拡張したものと捉えて良さそう


と思ったけど、Blitzの型定義ファイルの中にgetLayoutの定義があるのを見るに、Blitz公式としてこのメソッドを提供したいようだ

PageコンポーネントはBlitzPage型を使えばよいっぽい

app/pages/latest.tsx
import Layout from "app/layouts/Layout"
import { BlitzPage } from "blitz"

const Latest: BlitzPage = () => {
  return null
}

Latest.getLayout = (page) => <Layout title="">{page}</Layout>

export default Latest

じゃあデータを取りに行きます

app/hooks、名前が強い(Hooksがもうフレームワークのコアにいるんだなぁという気持ちにより)

ドキュメントを見るのが面倒なので、初期コードの中にそれっぽいコードがないかな〜と探索。 pages/index.tsxからuseCurrentUser()を呼んでいるっぽい。
その下にはuseMutation(logout)がある。

useCurrentUserの定義ファイル(app/hooks/useCurrentUser.ts)をみるとこの通り

export const useCurrentUser = () => {
  const [user] = useQuery(getCurrentUser, null)
  return user
}

useQueryってやつがReact向けのバインディングで、getCurrentUserがDBからデータを取っていそう。 こいつの中身はapp/users/queries/getCurrentUse.tsrに居る。

import { Ctx } from "blitz"
import db from "db"

export default async function getCurrentUser(_ = null, { session }: Ctx) {
  if (!session.userId) return null

  const user = await db.user.findFirst({
    where: { id: session.userId },
    select: { id: true, name: true, email: true, role: true },
  })

  return user
}

つまりそういうことだ(どういうこと????)

は〜いじゃあgetLatestEntries作りま〜〜〜す。

Blitzくんのおすすめディレクトリ構造気に入らないから僕はdomains/entriesに作ってみます〜〜〜〜 壊れたら従いま〜〜〜す

app/domains/entries/query.ts
import db from "db"

export default async function getLatestEntries(_ = null) {
  const entries = await db.entry.findMany()
  return entries
}

db.と打った時点でヌルッと.entryの補完が出てきて「どうやってんだこれ…」と思って'db'の参照元を見たらapp/db/index.tsに繋がってた… 何を言ってるかわからねえと思うが以下略

そしてgetCurrentUserの時は第二引数に{session}を取っていたけど、これはuseQuery(query, context)のcontextに渡したものが来るっぽい?
わかるよ、そのAPI気持ちいいよね(Fleurで第二引数以降のパスをやった人の顔)

Hooksもぬるりと作ります

app/hooks/useLatestEntries.ts
import getLatestEntries from "app/domains/entries/query"
import { useQuery } from "blitz"

export const useLatestEntries = () => {
  const [entries] = useQuery(getLatestEntries, null)
  return entries
}

oO (Mizchiさんの記事でサーバーサイドコードが出ないようになってるみたいな話を聞いた気がするけど別のフレームワークの話だったかな… あとから調べてみよう…

ドウシテ……

おい!!!!!!!!!お前お前お前お前お前〜〜〜〜〜〜〜〜〜〜〜!!!!!!!!!!!!!!(pages/index.tsxを開いた顔)

わかるよ(お気持ちの生き物)

あーそれでindex.tsxの中でHomeとUserInfoが別れてたのもあるのか
(Suspenseは親に居ないといけないので……)

どのリージョンをSuspenceにしたいかは完全にアプリの要件依存なので、フレームワーク開発者としては勝手にフォールバックはできんわな……

自分、涙、いいすか(男泣き)
解説: 同じ import { useQuery } from "blitz" しているコードなのにgetCurrentUser()は生きててgetLatestEntries()は死んでいるぞ!!!

ディレクトリ構造をapp/entries/queries/getLatestEntries.tsにしたら動きました。良い子は規約に従いましょう :sob:

(/appの直下にディレクトリ増えまくるのかなり嫌な気持ちになるのはRailsに飼いならされてるからなのか…?)

はい、じゃあ次は投稿を作っていきましょうね

さっき作った(絶望)app/entriesにmutations/createEntry.tsを作る

app/entries/mutations/createEntry.ts
import db, { Prisma } from "db"

export const createEntry = async ({ data }: { data: Prisma.EntryCreateArgs }) => {
  return await db.entry.create(data)
}

BlitzのドキュメントにはProjectCreateArgsを直接importするよう記載があるけど、生成されたコードではdeprecatedになっていたのでPrismaをimportするようにしている。


ここで Write Query Resolversを読んで「アッ、app/*/queriesに入ってれば何でもよい、なるほどね」となっています

書き込むページ作った(動作検証してない)

useMutation(mutationFunction)するとmutationするやつが得られるらしい
Entryに紐づくUserの型が通らなくて一瞬唸ったが、補完リストを見るとどうやらObjectのキーでリレーションに対して「つなげる・つくる・なかったらつくる」を切り分けられるらしい。

頭いいな(ほんとか? Componentにこんな大事な処理が散らかってたら「Railsもびっくり!型スラム!!!」になってしまうので多分ここらへんもうまいことmutation関数の中に閉じ込めないといつか死ぬぞ。 アプリ設計者の僕たちの頑張りどころだね!守ろう治安!!!)

import { createEntry } from "app/entries/mutations/createEntry"
import { useCurrentUser } from "app/hooks/useCurrentUser"
import { BlitzPage, useMutation } from "blitz"
import { useCallback, useRef } from "react"

const New: BlitzPage = () => {
  const [createEntryMutation] = useMutation(createEntry)
  const currentUser = useCurrentUser()

  const textareaRef = useRef<HTMLTextAreaElement>(null)

  const onSubmit = useCallback(async () => {
    if (!currentUser) return

    await createEntryMutation({
      data: {
        data: {
          content: textareaRef.current?.value!,
          user: { connect: currentUser },
        },
      },
    })
  }, [])

  return (
    <div>
      <textarea ref={textareaRef} placeholder="最後に言いたいことは?" />
      <button onClick={onSubmit} />
    </div>
  )
}

export default New

ドウシテ…

Suspence入れてなくて死んだので直した、だるびっしゅ…

import createEntry from "app/entries/mutations/createEntry"
import { useCurrentUser } from "app/hooks/useCurrentUser"
import { BlitzPage, useMutation } from "blitz"
import { Suspense, useCallback, useRef } from "react"

const Content = () => {
  const [createEntryMutation] = useMutation(createEntry)
  const currentUser = useCurrentUser()

  const textareaRef = useRef<HTMLTextAreaElement>(null)

  const onSubmit = useCallback(async () => {
    if (!currentUser) return

    await createEntryMutation({
      data: {
        data: {
          content: textareaRef.current?.value!,
          user: { connect: currentUser },
        },
      },
    })
  }, [currentUser])

  return (
    <div>
      <textarea ref={textareaRef} placeholder="最後に言いたいことは?" />
      <button onClick={onSubmit}>以上です</button>
    </div>
  )
}

const New: BlitzPage = () => {
  return (
    <Suspense fallback="matte">
      <Content />
    </Suspense>
  )
}

export default New

mutationの外部キーにはuniqueなフィールドだけを渡しましょう(懺悔)

user: { connect: { id: currentUser.id } },

Blitz完全に理解したわ、余裕余裕!!!!

このスクラップは2021/01/01にクローズされました
ログインするとコメントできます