Sveltekit・tRPC・Prismaで掲示板を作ってみる
ひとまず、開発完了。
リリースするために思ったより時間がかかってしまった。
技術構成
- SvelteKit
- TailwindCSS
- Headless UI
- Firebase Authentication
- Cloud Functions for Firebase
- tRPC
- Prisma
- PlanetScale
- Cloud Run
管理画面
- SvelteKit
- Tailwind CSS
- Material UI
- tRPC
- Prisma
OGP画像生成
- opentype.js
- sharp
- Twemoji
Sveltekitが正式にリリースされたので、モダンな掲示板を作ってみる。
技術選定
- Sveltekit
- tRPC
- Prisma
- PlanetScale
- Firebase Authentication
- TailwindCSS
- daisyUI
まずは認証。
Firebase Authentictionを使うけど、Sveltekitで共通の処理を書くにはレイアウトを使うと良いっぽい。
ログイン・ログアウトはできるようになった。
Firebaseでアカウントが作られたときに、データベースのUserテーブルにレコードを挿入するために、Firebase Functionsを使う。
firebase functionsをsveltekitのプロジェクト内で初期化したら、eslintでfunctionsフォルダ内のtsconfigを読んでくれない。
parserOptionsにtsconfigRootDir: 'functions'を追加したら大丈夫っぽい。
今度はeslintとprettierが競合した?
functionsフォルダ内にもprettier入れようかと思ったけど、面倒くさいので、とりあえず.prettierignoreに/functionsを追加して対応
やっぱりprettier欲しいので、eslint-config-prettierをfunctionsフォルダ内にもインストール。
.eslintrc.jsのextendsにprettierを追加して、rulesからquotesを削除して解決。
firebase deployするとCannot read file 'functions/tsconfig.json'と言われた
tsconfigRootDirを消して、.vscode/settings.jsonでeslint.workingDirectoriesを設定したら、直った。
{
"eslint.workingDirectories": ["./", "./functions"]
}
またfirebase deployでエラー
おそらくprisma client関連
prismaとcopyfilesをfunctionsディレクトリ内にインストールして、package.jsonのscriptsに"prisma:generate": "copyfiles ../prisma/* ./prisma && prisma generate"
を追加。npm run prisma:generate
をfunctionsディレクトリ内で実行したら、firebase deploy成功した。
最初は、package.jsonに
"prisma": {
"schema": "../prisma/schema.prisma"
}
って書いてやろうとしたけど、prisma generate
するとプロジェクトディレクトリ直下のnode_modulesに生成されるため、断念。
functionsのbuildスクリプトを"build": "npm run prisma:generate && tsc"
にして、firebase deploy
の実行時にprisma generate
を実行
無事アカウント作成したら、functionが実行されたけど、DATABASE_URLが無いと怒られた。functionsフォルダ内にも.envファイルを作成したら解決。
次は、スレッドを作成する機能を作る。
データベースの操作はサーバーサイドでPrismaでやるので、tRPCを使えるようにする。
とりあえず、今日はここまで。
残ってるタスクは
- スレッドへのいいね
- スレッドの削除
- スレッドへの投稿
- 投稿へのいいね
- 投稿へのリプライ
- 投稿の削除
- ユーザーページ
- スレッドのタグ検索
- スタイル調整
調べながらやってたら、思ったより時間がかかってしまった。アプリのベースはできてきたので、ガンガン進めていきたい。あと、今はtRPCを直接叩いている状態なので、TanStack Queryを使いたい。
今日できたところまでのスクショ↓
TanStack QueryがまだSvelteに対応していなかったので、svelte/storeで簡易的なSWRモドキを作ってみた。
import { get, writable } from 'svelte/store';
const swr = writable<{ key: unknown[]; data: unknown; fetchedAt: Date }[]>([]);
const refetch = async <T>(key: unknown[], queryFn: () => Promise<T>, index: number) => {
const data = await queryFn();
if (index == -1) {
swr.update(($swr) => [
...$swr,
{
key,
data,
fetchedAt: new Date()
}
]);
} else {
swr.update(($swr) => [
...$swr.slice(0, index),
{
key,
data,
fetchedAt: new Date()
},
...$swr.slice(index + 1)
]);
}
return data;
};
export const useSWR = async <T>(key: unknown[], queryFn: () => Promise<T>, maxAge = 600) => {
let data: T;
const $swr = get(swr);
const chachedQueryIndex = $swr.findIndex(
(query) => JSON.stringify(query.key) == JSON.stringify(key)
);
const chachedQuery = $swr[chachedQueryIndex];
if (!chachedQuery || (Number(new Date()) - Number(chachedQuery.fetchedAt)) / 1000 > maxAge) {
refetch(key, queryFn, chachedQueryIndex);
}
if (chachedQuery) {
if ((Number(new Date()) - Number(chachedQuery.fetchedAt)) / 1000 > maxAge) {
refetch(key, queryFn, chachedQueryIndex);
}
data = chachedQuery.data as T;
} else {
data = await refetch(key, queryFn, chachedQueryIndex);
}
return data;
};
daisyUIを使うとeslintがうるさいので、headless uiを使ってみる。
スレッドのいいねと削除機能ができた。
次はいよいよ書き込み機能をつくる。
ユーザーページを作る。
- 公開されるプロフィール
- 作成したスレッド
- スレッドへの投稿
- ライブラリ
- スレッドと投稿へのいいね
- 設定
- プロフィール
- アカウント削除
ユーザーページが完成。
リプライ機能は作ってないけど、ほとんど完成した。
かかった時間はだいたい3日。
Svelte・Tailwind・Headless UIでフロントの開発が楽だったのもあるけど、なによりtRPC・Prismaの開発体験が良すぎて、めっちゃ簡単に機能追加できた。あと、認証をFirebaseに丸投げできたのも大きい。
最終的な技術構成
- SvelteKit
- TailwindCSS
- Headless UI
- Firebase Authentication
- Cloud Functions for Firebase
- tRPC
- Prisma
- PlanetScale
報告機能を作る。
スレッドと投稿で別々の報告用テーブルを作成。
報告機能が完成。
ラジオボタンにtailwindのformsプラグイン使おうとしたけど、意外と使い勝手が悪かったので、自前で実装。
スレッドの共有機能が完成。
あとは、規約書いて、Metaタグ書けば終わりかな。
管理アプリ作るために、モノレポ化。
firebase functionsはデプロイ時にnpm installするためprisma generateを事前にする必要はなかった。でも、firebase.jsonのsourceで指定したディレクトリ以下しかアップロードされないためschema.prismaをコピーする必要はある。schema.jsonをルートディレクトリにおいたら、copyfilesではパスをうまく解決できなかったので、ncpに変えた。
管理アプリはmaterial uiとtailwindを使って開発。
monorepoにしたおかげで、prismaのスキーマを共有できて、楽に開発できた。
trpcを使うために、trpc-sveltekitを使っていたが、ただの薄いラッパーだった上に、型が合わなくなるので、直接trpcを使うことにした。
Cloud Runに開発環境をデプロイ。
Cloud Load Balancing使って、IAPでアクセス制限。
問題なく動作することを確認。
あとはアクセスログの記録と、SEO対策(OGPの動的生成とかしたい)したら開発はひとまず終了。
規約も書かないといけないけど。
OGP画像の動的生成はopentype.jsとsharpで実装。
1文字ずつ処理することで、複数のフォントに対応。
長い文章の省略にも対応。
Intl.Segmenterで単語に分けて改行。
1行に入り切らない単語は、単語の途中で改行。
絵文字はemoji-regexで取得してから、Twemojiの画像を読み込んで表示。
CJKの文章は単語の途中でも改行するようにした。
CJKの文字の判定は
text.matchAll(/\p{scx=Hani}|\p{scx=Hira}|\p{scx=Kana}|\p{scx=Hang}/gu);
という正規表現でできる。
なぜかCloud Runで動かすと絵文字が分割された。
ローカルで実行すると、正しく表示される。
コンテナで実行してるので環境差異はないはずなんだけど・・・
原因はよくわからないけど、emoji-regexからtwemoji-parserに変えたら直った。