先週のブログRemixはReact界のRuby on Railsか?の中でふれた Jamstack用Fullstack Frameworkを試してみたけど時期尚早だと思ったですが、もう1年6か月も経ったので、再びBlitz、RedwoodJSを評価してみる事にしました。今回はBlitzです。
Blitzの特徴
Blitzのホームページには以下の6つのキーワードが書かれています。
Fullstack & Monolithic
JavaScript(TypeScript)を使ったバックエンド、フロントエンドを含むフルスタックのフレームワークです。
- バックエンドはPrismaを使ったデーターベースアクセスをサポートしています。
- フロントエンドはReactです、ホック(Hooks)やSuspenseなど新しい技術も使っています
- 認証(ログイン)機能なども内蔵しています
API Not Required
バックエンドとフロントエンドの通信はフレームワークの中で行われ、アプリ開発者は通信コードを書く必要はありません。前回のRemixと同じですね。
Loose Opinions
フレームワークで使われているライブラリーはプラガブルで置き換える事ができます。たとえばフォームの便利ライブラリーはreact-final-form, react-hook-form, formikから選べます。
Convention over Configuration
Ruby on Rails同様に設定より規約です、Ruby on Railsに大きく影響を受けています。
Easy to Start, Easy to Scale
初心者に優しく、少ないコードでスケールしやすいそうです。
Stability
バージョン1.0(現状はバージョン0.45.4)以降は、安定した定期的リリースサイクルに切り替え、stable、LTS(Long-Term Support)、beta等を用意する予定でだそうです。
前回と同様なジャンケンのアプリを作ってみた
作成手順
1. プロジェクト作成
Ruby on Rails同様にblitz new
コマンドでプロジェクトを作ります、いくつかオプションを設定しました。
- 言語はTypeScript
- テンプレートはDBアクセスや認証を含む
full
- フォーム
<form>
には推奨されているreact-final-formを選択 - コマンドは
npm
を選択(yarn
も選べます)
$ npx blitz new jyanken-blitz --language=typescript --template=full --form=react-final-form --npm
2. generate all
Ruby on Railsのrails generate scaffold
相当のジェネレーターを使い、モデルの定義を指定し、データーベースやMVCのコードを生成します。
$ npx blitz generate all jyanken computer:int human:int judgment:int
3. 開発開始
開発用サーバーが起動され、webpackを使った開発環境が立ち上がります。
$ npm run dev
フォルダー構成
blitz generate all
で生成されたファイル類は以下のようになります。
app/
├── api
├── auth
・・・省略・・・
├── core
│ ├── components
│ │ ├── Form.tsx フォームの共通コンポーネント
│ │ └── LabeledTextField.tsx 上で使われるテキスト入力タグのコンポーネント
・・・省略・・・
│ └── layouts
│ └── Layout.tsx 各ページで使われるレイアウトファイル
├── jyankens
│ ├── components
│ │ └── JyankenForm.tsx 作成・更新で使われるフォームのコンポーネント
│ ├── mutations
│ │ ├── createJyanken.ts バックエンドのJyankenレコード作成関数
│ │ ├── deleteJyanken.ts バックエンドのJyankenレコード削除関数
│ │ └── updateJyanken.ts バックエンドのJyankenレコード変更関数
│ └── queries
│ ├── getJyanken.ts バックエンドのJyanken1レコード取得関数
│ └── getJyankens.ts バックエンドのJyanken全レコード取得関数
├── pages
・・・省略・・・
│ └── jyankens
│ ├── [jyankenId]/edit.tsx 変更ページのコンポーネント
│ ├── [jyankenId].tsx 1件表示ページのコンポーネント
│ ├── index.tsx 一覧表示ページのコンポーネント
│ └── new.tsx 新規作成ページのコンポーネント
└── users
・・・省略・・・
以下の部分は省略しました。
- 認証関連のコンポーネント、バックエンド
- データーベース定義(マイグレーション)
- 設定ファイル
blitz generate allで生成されたコード
blitz generate all
で生成されたコードの一部を示します。下のページは一覧表示ページのReactコンポーネントです。
- ①
usePaginatedQuery()
は情報取得系のバックエンド関数を呼び出すホック(Hooks)です - ②
usePaginatedQuery()
では条件、ソート等を指定できます - ③ 一覧表示ページには最初からページング機能があります
- ④ Ruby on Railsと違いテーブルのカラムに対応する表示やフォームは生成されずに
.name
決め打ちです - ⑤ React18で正式サポートされたSuspenseを使っています、素晴らしい!
app/pages/jyankens/index.tsx
import { Suspense } from "react"
import { Head, Link, usePaginatedQuery, useRouter, BlitzPage, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import getJyankens from "app/jyankens/queries/getJyankens"
const ITEMS_PER_PAGE = 100
export const JyankensList = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
const [{ jyankens, hasMore }] = usePaginatedQuery(getJyankens, { // ← ①
orderBy: { id: "asc" }, // ← ②
skip: ITEMS_PER_PAGE * page, // ← ③
take: ITEMS_PER_PAGE, // ← ③
})
const goToPreviousPage = () => router.push({ query: { page: page - 1 } })
const goToNextPage = () => router.push({ query: { page: page + 1 } })
return (
<div>
<ul>
{jyankens.map((jyanken) => (
<li key={jyanken.id}>
<Link href={Routes.ShowJyankenPage({ jyankenId: jyanken.id })}>
<a>{jyanken.name}</a> // ← ④
</Link>
</li>
))}
</ul>
<button disabled={page === 0} onClick={goToPreviousPage}> // ← ③
Previous
</button>
<button disabled={!hasMore} onClick={goToNextPage}> // ← ③
Next
</button>
</div>
)
}
const JyankensPage: BlitzPage = () => {
return (
<>
<Head>
<title>Jyankens</title>
</Head>
<div>
<p>
<Link href={Routes.NewJyankenPage()}>
<a>Create Jyanken</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}> // ← ⑤
<JyankensList />
</Suspense>
</div>
</>
)
}
JyankensPage.authenticate = true
JyankensPage.getLayout = (page) => <Layout>{page}</Layout>
今回修正したコード
Ruby on Railsと同様にblitz generate all
で生成されたコードだけではアプリになりませんので、必要なファイルを変更します。
app/pages/jyankens/index.tsx
以下の画像のような、ジャンケン結果の一覧表示ページのReactコンポーネントです。
もとは上で説明したコードです、見た目以外で変更した部分などは
- ①
.authenticate = false
このページはログインしてなくても表示できるようにしています - ページング機能は取ってしましました
import { Suspense } from "react"
import { Head, Link, usePaginatedQuery, BlitzPage, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import getJyankens from "app/jyankens/queries/getJyankens"
export const JyankensList = () => {
const [{ jyankens }] = usePaginatedQuery(getJyankens, {
orderBy: { id: "desc" },
})
const teString = ["グー", "チョキ", "パー"]
const judgmentString = ["引き分け", "勝ち", "負け"]
const tableStyle: React.CSSProperties = { marginTop: 20, borderCollapse: "collapse" }
const thStyle: React.CSSProperties = { border: "solid 1px #888", padding: "3px 15px" }
const tdStyle: React.CSSProperties = {
border: "solid 1px #888",
padding: "3px 15px",
textAlign: "center",
}
return (
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>あなた</th>
<th style={thStyle}>コンピュター</th>
<th style={thStyle}>勝敗</th>
</tr>
</thead>
<tbody>
{jyankens.map((jyanken, ix) => (
<tr key={ix}>
<td style={tdStyle}>{teString[jyanken.human]}</td>
<td style={tdStyle}>{teString[jyanken.computer]}</td>
<td style={tdStyle}>{judgmentString[jyanken.judgment]}</td>
</tr>
))}
</tbody>
</table>
)
}
const JyankensPage: BlitzPage = () => {
return (
<>
<Head>
<title>Jyakens</title>
</Head>
<div>
<p>
<Link href={Routes.NewJyankenPage()}>
<a>対戦</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<JyankensList />
</Suspense>
</div>
</>
)
}
JyankensPage.authenticate = false // ← ①
JyankensPage.getLayout = (page) => <Layout>{page}</Layout>
export default JyankensPage
app/jyankens/queries/getJyankens.ts
バックエンドのJyanken全レコード取得関数も少し変更しています。
- ①
resolver.authorize()
をコメントアウトする事でこの関数がログインしてなくても動作するように設定しました - ② PrismaのfindManyメソッドで条件にあう全レコードを取得
import { paginate, resolver } from "blitz"
import db, { Prisma } from "db"
interface GetJyankensInput
extends Pick<Prisma.JyankenFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
export default resolver.pipe(
// resolver.authorize(), // ← ①
async ({ where, orderBy, skip = 0, take = 100 }: GetJyankensInput) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const {
items: jyankens,
hasMore,
nextPage,
count,
} = await paginate({
skip,
take,
count: () => db.jyanken.count({ where }), // ← ②
query: (paginateArgs) => db.jyanken.findMany(
{ ...paginateArgs, where, orderBy }), // ← ③
})
return {
jyankens,
nextPage,
hasMore,
count,
}
}
)
app/pages/jyankens/new.tsx
以下の画像のような、ジャンケンを行う新規作成ページのReactコンポーネントです。
今回は、blitz generate all
で生成されたコードを尊重しジャンケンページは、結果一覧ページは別にしました。
- ①
useMutation()
は情報更新系のバックエンド関数を呼び出すホック(Hooks)です - ② ジャンケン実行後は上のジャンケン結果の一覧表示ページに移動します
- ③ このページもログインしてなくても表示できるように設定しています
import { Link, useRouter, useMutation, BlitzPage, Routes } from "blitz"
import Layout from "app/core/layouts/Layout"
import createJyanken from "app/jyankens/mutations/createJyanken"
import { JyankenForm, FORM_ERROR } from "app/jyankens/components/JyankenForm"
const NewJyankenPage: BlitzPage = () => {
const router = useRouter()
const [createJyakenMutation] = useMutation(createJyanken) // ← ①
return (
<div>
<h1>ジャンケンポン</h1>
<JyankenForm
submitText="ポン"
onSubmit={async (values) => {
try {
await createJyakenMutation(values)
router.push(Routes.JyankensPage()) // ← ②
} catch (error: any) {
console.error(error)
return {
[FORM_ERROR]: error.toString(),
}
}
}}
/>
<p>
<Link href={Routes.JyankensPage()}>
<a>戻る</a>
</Link>
</p>
</div>
)
}
NewJyankenPage.authenticate = false // ← ③
NewJyankenPage.getLayout = (page) => <Layout title={"Create New Jyaken"}>{page}</Layout>
export default NewJyankenPage
app/jyankens/components/JyankenForm.tsx
ジャンケンを行うフォームのReactコンポーネントです。
- ① react-final-formのFieldコンポーネントを使いラジオボタンを表示しています
import { Form, FormProps } from "app/core/components/Form"
import { z } from "zod"
export { FORM_ERROR } from "app/core/components/Form"
import { Field } from "react-final-form"
export function JyankenForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
return (
<Form<S> {...props}>
<div>
<label>
<Field name="human" component="input" type="radio" value="0" /> // ← ①
グー
</label>
<label>
<Field name="human" component="input" type="radio" value="1" /> // ← ①
チョキ
</label>
<label>
<Field name="human" component="input" type="radio" value="2" /> // ← ①
パー
</label>
</div>
</Form>
)
}
app/jyankens/mutations/createJyanken.ts
バックエンドのJyankenレコード作成関数も少し修正しました。
- ① Blitzはフォーム入力バリデーションにはZodを使っています。ここでは0,1,2という文字のみ有効です
- ② バリデーションの実行
- ③ コメントアウトする事で、ログインしてなくても動作するように設定しました
- ④ Prismaのcreateメソッドでレコードを作成
import { resolver } from "blitz"
import db from "db"
import { z } from "zod"
const CreateJyanken = z.object({ // ← ①
human: z.enum(["0", "1", "2"]),
})
export default resolver.pipe(
resolver.zod(CreateJyanken), // ← ②
// resolver.authorize(), // ← ③
async (input) => {
const human = Number(input.human)
const computer = Math.floor(Math.random() * 3)
const judgment = (computer - human + 3) % 3
const jyaken = await db.jyanken.create(
{ data: { human, computer, judgment } }) // ← ④
return jyaken
}
)
通信
Blitzのフロントエンドとバックエンド間の通信は、REST でも GraphQLでもなく、その中間のような方式です。
- GraphQLのような言語は使っていない
- POSTリクエストのみを利用(GraphQL風)
- 機能名はURLで指定 (REST風)
- データ、パラメーターはJSON
getJyankens
Jyanken全レコード取得関数
- リクエスト
- params: パラメーター
- ここでは、SELECT文の
ORDER BY id DESC
を指定しています
- ここでは、SELECT文の
- params: パラメーター
POST http://localhost:3000/api/rpc/getJyankens
送信データ: {"params":{"orderBy":{"id":"desc"}},"meta":{}}
- レスポンス
- result: 結果テータ
- error: エラーメッセージ
- meta: データの型情報など
- createdAt/updatedAtは文字列ですが
Date
型を表します
- createdAt/updatedAtは文字列ですが
{
"result": {
"jyankens": [
{
"id": 5,
"createdAt": "2022-07-14T02:06:41.131Z",
"updatedAt": "2022-07-14T02:06:41.131Z",
"computer": 2,
"human": 2,
"judgment": 0
},
{
"id": 4,
"createdAt": "2022-07-13T07:49:43.666Z",
"updatedAt": "2022-07-13T07:49:43.666Z",
"computer": 1,
"human": 1,
"judgment": 0
},
・・・省略・・・
],
"nextPage": null,
"hasMore": false,
"count": 5
},
"error": null,
"meta": {
"result": {
"values": {
"jyankens.5.createdAt": [
"Date"
],
"jyankens.5.updatedAt": [
"Date"
],
・・・省略・・・
}
}
}
}
createJyanken
バックエンドのJyankenレコード作成関数
- リクエスト
- params: パラメーター
- ここでは、人間が選択した手を送っています
- params: パラメーター
POST http://localhost:3000/api/rpc/createJyanken
送信データ: {"params":{"human":"2"},"meta":{}}
- レスポンスは省略
まとめ
Blitzは完成度高い、フルスタックのフレームワークです。最初から認証(ログイン)を内蔵していて、実用的なアプリが短期間で作成できそうです。使っている技術はNext.js, Prisma, Zodなどメジャーなものです。
Ruby on Railsのようなジェネレーター(generator)があり、Rails同様に効率良くアプリが作れます、ただし生成されるコードにはRDBのカラムに対応する表示・入力等は無く、完成度はRailsに比べると低いです。
ただし、バックエンドのサーバーとフロントエンドとの通信は独自方式なので、サービス(アプリ)が発展しバックエンド側を強化するさいのネックにならないのか私は気になります。また現在のドキュメントは弱く、今回のサンプルコードを作るさいにも困りました。
次回はRedwoodJSです。