フルスタックWebフレームワーク「RedwoodJS」のチュートリアルをやってみた
概要
こちらの記事からRedwoodJSを知り、おもしろそう〜と思ってチュートリアルをやってみました。
先に感想を書くかたちにはなりますが、結論としては、RedWoodJSが提供する各機能をわかりやすく学べて、いいチュートリアルでした。難易度が低くてよかったです。
この記事では、RedwoodJSチュートリアルを章ごとにかんたんにご紹介します。
次の段落で述べていますが、モダンな技術スタックで構成されており、かつ、ところどころRailsに似た思想を持ったフレームワークなので、触って楽しめる人は多いのではないかと思います。
RedwoodJSとは?
RedwoodJSとは、TypeScriptをサポートしている、フルスタックのWebフレームワークです。
こちらは公式ページからの引用スクショですが、React, GraphQL, Prismaなど、なかなかイケイケな技術で構成されています。
そしてさらに特徴的なのが、設計思想が少しRailsに似ている点でしょう。
たとえば、冒頭の記事でも触れられていますが、RedwoodはRedwoodRecordというORMを持っています。
この機能は「SQL構文を使わず、かんたんな形式でDBを操作できる」という点で、RailsのActiveRecordに大きく影響を受けています。
これは誰か使ってみた人が「似てるね!」と推測で言ったわけではなく、以下のように、公式が明言している内容です。
RedwoodRecord is heavily inspired by ActiveRecord which ships with Ruby on Rails. It presents a natural interface to the underlying data in your database, without worry about the particulars of SQL syntax.
その他にも、scaffoldコマンドでなんやかんやいろいろ作れる点や、単数/複数の命名規則が導入されている点など、Railsの設計思想に近い箇所はいくつか見受けられます。
1章
それでは、チュートリアル各章の紹介をしていきます。
まずChapter1では、環境構築とかんたんなページ作成などを行っています。
yarn create redwood-app
コマンドを叩くと、以下の画面が迎えてくれます。
docなど関連ページに飛べるようになっていていいですね。あとデザインが個人的にけっこう好みです。
ページ生成はyarn redwood generate page
コマンドで行います。
これを叩くと、「ページ生成(React)」「テストコード生成(Jest, Storybook)」「ルーティング設定」が一気に実行されます。
自動で生成される分、「テストはもちろん書くよね?」という公式からの圧を強く感じます。
ルーティングは以下のように制御しています。Reach RouterやReact Routerに少し似ていますね。
const Routes = () => {
return (
<Router>
<Route path="/" page={HomePage} name="home" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
2章
続いて2章です。2章ではざっくり、DBの読み取り・書き取りのやり方を扱っています。
Redwoodでは前述の通り、Prismaを使ってDBと対話しています。
以下はデータ構造を定義している、Prisma製Schemaファイルの一部です。
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}
上記はPostというテーブルの定義です。それぞれデータ名、型、特筆事項を設定しています。
それぞれPrismaに慣れていなくても読めそうですね。
idは自動でインクリメントしていき、createdAtには現在時刻が入っています。
そして、こうして規定したschemaファイルにもとづき、専用のコマンドを叩いてmigrateを行います。
作成されたデータベースは、これまた専用のコマンドを叩くと、ローカルのPrisma Studioでこんな風に確認できます。
またこの章では、CellというRedwood独自の機能も紹介されています。
Cellはデータ取得をまるっと扱うコンポーネントです。
具体的には、Cellをレンダリングすると、RedwoodはGraphQLのクエリを実行し、応答があるまでLoading、成功するとSuccess、失敗するとFailureコンポーネントを表示してくれます。
なにやら便利そうですね。Cellの実際のコードはこんな感じです。
PostCell
import type { FindPostById } from 'types/graphql'
import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web'
import Post from 'src/components/Post/Post'
export const QUERY = gql`
query FindPostById($id: Int!) {
post: post(id: $id) {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Post not found</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error.message}</div>
)
export const Success = ({ post }: CellSuccessProps<FindPostById>) => {
return <Post post={post} />
}
3章
3章ではForm作成を扱っています。
Redwoodでは、Form, Label, TextField, SubmitHandlerなど、Form作成に必要なものはほぼ全てRedwood自身からimportすることができます。
また、各コンポーネントはvalidationの機能もデフォルトで対応しており、たとえばTextFieldにはこのように必須条件をつけます。
<Form onSubmit={onSubmit}>
<label htmlFor="name">Name</label>
<TextField name="name" validation={{ required: true }} />
<label htmlFor="email">Email</label>
<TextField name="email" validation={{ required: true }} />
<label htmlFor="message">Message</label>
<TextAreaField name="message" validation={{ required: true }} />
<Submit>Save</Submit>
</Form>
RedwoodのFormはReact Hook Formを土台に作られており、書き方も似ているので、馴染みがある方にはより使いやすい内容かも知れません。
この章ではform用のwrite処理、つまりGraphQLのmutationの定義・使用も扱っています。
4章
4章では認証とデプロイを扱っています。
認証の内容はめちゃくちゃかんたんです。
まず、一般的に「Private Route」と呼ばれる、ログインした人だけを入れるようにするページ設定は、ルーティング用のファイルに2行書き足すだけで設定できます。
以下の<Private>
タグで対象のRouteをラップすることで、それを実現します。プロパティのunauthenticatedは、未認証だったときにリダイレクトするページを指定するものです。
import { Private, Router, Route, Set } from '@redwoodjs/router'
import PostsLayout from 'src/layouts/PostsLayout'
const Routes = () => {
return (
<Router>
<Private unauthenticated="home">
<Set wrap={PostsLayout}>
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}/edit" page={PostEditPostPage} name="editPost" />
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
<Route path="/admin/posts" page={PostPostsPage} name="posts" />
</Set>
</Private>
<Route path="/" page={HomePage} name="home" />
</Router>
)
}
そして、認証の肝であるLoginとSignupのUI・ロジックは、yarn rw g dbAuth
というコマンドを叩くだけで、まるっと作成してくれます。けっこうすごいですね。
こちらがサインアップ用の画面です。もちろんUIを自分で作りたい場合や、SNS対応したい場合は、ここからもう何手間も必要ですが、1コマンドでこれを作れるのがすごいです。
さて、デプロイはRailway(DB)とNetlify(サーバー)を使って実施しますが、これらはRedwood本体の説明からは外れるので省略します。
ちなみにこちらも設定は一瞬で、非常にかんたんにデプロイすることができました。
5章
5章ではStorybookとテストを扱っています。
Storybookの確認ページもデフォルトでサポートしているため、コマンドを叩くと見れるようになります。
テストにはJestだけでなく、React Testing Libraryも使用します。やはりライブラリが充実していますね。
テスト含め、TypeScriptでWebサービスを作る上で必要なだいたいのものが、Redwoodではデフォルトで搭載されていて、「Redwoodにさえ乗っておけば問題ないよね?」という公式の思想がなんとなく伝わってくる気がします。
6章
6章ではComponentという、ページの構成要素となる単位のコードを使って、Comment機能を作成します。
Component自体は専用のコマンドを叩いて雛形を作り、デコレートしていくのですが、それとは別に注目したいのは、この章で新たに追加したSchemaファイルにおけるComment型の定義です。
PostとCommentは1対多という関係性なのですが、それをRedwoodでは以下のように表現します(Post内のcommentsと、Comment内のpostが該当箇所となります)。
model Post {
id Int @id @default(autoincrement())
title String
body String
comments Comment[]
createdAt DateTime @default(now())
}
model Comment {
id Int @id @default(autoincrement())
name String
body String
post Post @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())
}
「Postは複数のcommentを所有している」という内容は、単にcommentsの型を、Comment型の配列として規定することで実現しており、シンプルですね。
また「Commentは1つのPostに所有されている」という内容は、relationというキーワードを使ってその関係性を表現しています。
postのidであるpostIdを関連づけることで、CommentからPostを参照できる構造になっています。
これはRailsでいうhas_masyとbelongs_toの関係ですが、Redwoodではこのように表現されるようです。
7章
7章ではRole-Based Access Control(RBAC)について扱っています。
RBACはアクセス制御方式の一つで、ユーザーの属性・状況にもとづいてではなく、割り当てたロール(役割)をもとに、システムがユーザーに権限を付与する、という仕組みです。
RedwoodはRBACに則って開発可能とのことで、この章ではadminロールと、悪意あるコメントの削除を担当するmoderatorロールを設定していました。
気になった点
各章の説明はだいたいそんな感じで、基本スムーズに進んだのですが、中には「これどうすればいいんだろう?」となる箇所もありました。
1つ目
ルーティングで、pathの間にpostIdなどのパラメータを挟みたいとき、Routing用ファイルをこんな感じで書いてねとのことでした(id:Intの箇所)。
import { Private, Router, Route, Set } from '@redwoodjs/router'
import PostsLayout from 'src/layouts/PostsLayout'
const Routes = () => {
return (
<Router>
<Set wrap={PostsLayout}>
<Route path="/admin/posts/new" page={PostNewPostPage} name="newPost" />
<Route path="/admin/posts/{id:Int}" page={PostPostPage} name="post" />
</Set>
</Router>
)
}
その通りにすると、Routeのpathのところで「それは受け付けられない」と型で怒られてしまいました。
結局直し方がわからず、チュートリアルだしいっかということで、web-routerRoutes.d.tsというルーティングファイルの型を扱っている、奥底にある定義ファイルを修正しました(たぶんやばい)。
なので、このルーティングのpathの型は、本来どのようにして合わせるものだったのかが、いまいち分からなかったです。
2つ目
ローカルでのDBはsqliteを使っており、デプロイの際にpostgreに変更するのですが、その際、それまでのmigrationファイルの内容が自動でpostgre仕様に変わらずに、SQLでのsyntaxエラーを吐かれました。
こちらも手動でファイルを修正したので、そのあたりの修正もコマンドで対応してくれるとうれしかったです(自分が見落としてる説もありますが)。
所感
最近流行っている技術のごった煮、という感じで、触っていておもしろかったです🙌
特にPrismaなんかは使ったことがなかったので、この機会に触ることができてよかったです。
そしてやはり、フロントもバックエンドも同じ言語で書けるのは強いなと思いました🤔
Discussion