Closed32

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

ピン留めされたアイテム
HanaklaHanakla

#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あたりの設計をどうするかというのがアプリ開発者の課題になりそう
HanaklaHanakla

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

HanaklaHanakla

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

HanaklaHanakla

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でタイムゾーンが異なったときに死ぬという問題に当たりそう)

HanaklaHanakla

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

HanaklaHanakla

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

HanaklaHanakla

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

HanaklaHanakla

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

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


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

HanaklaHanakla

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
HanaklaHanakla

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
}

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

HanaklaHanakla

は〜いじゃあ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で第二引数以降のパスをやった人の顔)

HanaklaHanakla

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
}
HanaklaHanakla

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

HanaklaHanakla

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

HanaklaHanakla

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

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

HanaklaHanakla

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

HanaklaHanakla

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

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

HanaklaHanakla

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

HanaklaHanakla

さっき作った(絶望)app/entriesmutations/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に入ってれば何でもよい、なるほどね」となっています

HanaklaHanakla

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

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
HanaklaHanakla

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
HanaklaHanakla

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

user: { connect: { id: currentUser.id } },
このスクラップは2021/01/02にクローズされました