この記事は、木更津高専 Advent Calendar 2024 17日目の記事です。
←前 Minio×Hono×Reactをやろうとした経験談を書いておこうと思ふ。 by @nairoki
Vikeとかいうフレームワークがなんか良さそうなので調べながら何かつくってみました
Vikeの特徴
Vike公式サイトにはこのような記述がある
要するに好きなフレームワークを自分で組み合わせられるということらしい。自由度が高いのは個人的に好き。
どのくらい自由かは公式プロジェクトジェネレーターの「Bâti」を使えばわかった
公式ジェネレーター Bâti
Bâtiを使うと簡単にVikeプロジェクトを始められるっぽい。
ここにある選択肢から自由に選べる(もちろんVueとMantineとかはエラーでる)
上に出てくるコマンド(npm/yarn/pnpm/bun ~
)を使えば手元のPCで開発を始められる。
また、「Try me in Stackblitz」を押すと、選んだ構成がStackblitzで開かれ、ブラウザ上で簡単に試せる。
Presetを使うと手軽に設定できるらしく「Next.js」や「Nuxt」を選ぶとReactやVueを使ったそれっぽい構成になる。
Telefunc って何だろうと思ったらVike開発者が開発したサーバーサイドの処理をクライアントから呼び出せるやつらしい
とりあえず何か作ってみる
なんか作ったほうが理解を深められると思うのでブログシステムでも作ってみる。
pnpm create bati --react --tailwindcss --shadcn-ui --lucia-auth --telefunc --hono --drizzle --biome
実行するとプロジェクトができる
ファイル構造
(sqlite.dbはpnpm dev
した後にできた)
ページ
デフォルトページの機能なんてカウンターだけかと思ったら、DBを使うTodoやTelefuncを使ったData Fetching、更にLuciaを使ったログインページまでもが生成されていた。これなら初心者でも安心?
+config.ts
レイアウトやタイトルなどの設定などができる。
例えばrootのpages/+config.ts
はこんな感じ
import vikeReact from "vike-react/config";
import type { Config } from "vike/types";
import Layout from "../layouts/LayoutDefault.js";
// Default config (can be overridden by pages)
// https://vike.dev/config
export default {
// https://vike.dev/Layout
Layout,
// https://vike.dev/head-tags
title: "My Vike App",
description: "Demo showcasing Vike",
passToClient: ["user"],
extends: vikeReact,
} satisfies Config;
本来+config.ts
で指定するページやレイアウトのコンポーネントを指定するが、+Page.tsx
や+Layout.tsx
を置くことでも代用できる。
また、+config.ts
を特定のディレクトリ下に置くと、そのディレクトリ下のみにConfigが適用される。
レイアウトを変える
config.ts
を見ると分かる通りレイアウトは"../layouts/LayoutDefault";
にある。
ログイン時は投稿ページへのリンクとログアウトボタン、未ログイン時はログインページへのリンクを表示するようにしてみる。
ログイン中ユーザーの情報はpageContextオブジェクトから得られる。
pageContextには現在のページに関する情報がある。
また、ミドルウェアを使って自分で好きなプロパティを設定できる。
export default function LayoutDefault({
children,
}: {
children: React.ReactNode;
}) {
const { user } = usePageContext();
return (
<div className={"flex max-w-5xl m-auto"}>
<Sidebar>
<Logo />
<Link href="/">View Posts</Link>
{user ? (
<>
<Link href="/new-posr">New Post</Link>
<button
className="text-left"
onClick={async () => {
await fetch("/api/logout", { method: "POST" });
navigate("/");
}}
>
Logout
</button>
</>
) : (
<Link href="/login">Login</Link>
)}
</Sidebar>
<Content>{children}</Content>
</div>
);
}
簡単!
ログインしてないと見れないページを作る
よくある、ログイン時は見れて、それ以外はログインページにリダイレクト のような動作はguard
で簡単に実現できる。
今回は投稿ページを例に実装する。
まずpages/
以下に+guard.ts
と+Page.tsx
を作る
+guard.ts
には以下のように書くだけ!
// https://vike.dev/guard
import { redirect } from "vike/abort";
import type { GuardAsync } from "vike/types";
const guard: GuardAsync = async (pageContext): ReturnType<GuardAsync> => {
if (!pageContext.user) {
throw redirect("/login");
}
};
export { guard };
これだけで/new-post
がログインユーザーのみアクセス可能になった。
DBに追加操作をする
Telefuncを使ってサーバーサイドでDBを操作する。
import { Abort, getContext } from "telefunc";
import * as drizzleQueries from "../../database/drizzle/queries/posts";
export async function onCreatePost({ title, content }: { title: string, content: string }) {
const {session,db,lucia} = getContext();
if (!session) {
return {not_logged_in: true}
}
const result = await lucia.validateSession(session.id);
if (!result) {
return {not_logged_in: true}
}
await drizzleQueries.insertPost(db, title,content, session.userId);
}
呼び出し側はこう書くだけ
async function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
// ✅ This will be type-safe and validated.
console.log(values);
const result = await onCreatePost(values);
if (result?.not_logged_in) {
navigate("/login");
return;
}
form.reset();
mdxRef.current?.setMarkdown("");
}
DBからデータを取得し表示する
データを取得し、それをページに渡すにはdata()
を使う
import type { PageContext } from "vike/types"
import * as drizzleQueries from "../../database/drizzle/queries/posts";
export async function data(pageContext:PageContext) {
const posts = await drizzleQueries.getAllPostAbstracts(pageContext.db)
return {
posts
}
}
export type Data = Awaited<ReturnType<typeof data>>
また、ページ側でそれを受け取るにはuseData()
を使う
export default function Page() {
const { posts } = useData<Data>();
return (
<>
<h1 className={"font-bold text-3xl pb-4"}>All Posts</h1>
{posts.map((post) => (
<PostCard post={post} key={post.id} />
))}
</>
);
}
ブログっぽくなった
パスパラメーターを使う
ユーザー用のページを/users/<ユーザー名>
に置きたいときは、pages/users/@username
のようにディレクトリを作成する。
同じように+data.ts
や+Page.tsx
を書く
import type { PageContext } from "vike/types"
import * as drizzleQueries from "@/database/drizzle/queries/posts";
import { render } from "vike/abort";
import { getExistingUser } from "@/database/drizzle/queries/lucia-auth";
export async function data(pageContext:PageContext) {
const username = pageContext.routeParams.username
const user = await getExistingUser(pageContext.db,username)
if(!user) {
throw render(401)
}
const posts = await drizzleQueries.getUserPostAbstracts(pageContext.db,user.id)
return {
posts
}
}
export type Data = Awaited<ReturnType<typeof data>>
import { useData } from "vike-react/useData";
import type { Data } from "./+data";
import { usePageContext } from "vike-react/usePageContext";
import { PostCard } from "@/components/PostCard";
export default function Page() {
const pageContext = usePageContext();
const { posts } = useData<Data>();
return (
<>
<h1 className={"font-bold text-3xl pb-4"}>{pageContext.user?.username}'s Posts</h1>
{posts.map((post) => (
<PostCard post={post} key={post.id} />
))}
</>
);
}
同じように投稿詳細ページも作る
pages/posts/@id
下に+data.ts
や+Page.tsx
を書くと・・・
できた!
ブログだ!!
おわり
ほんとはSSR→SSGが簡単にできるんだぞってことを見せたかったんですが、Drizzleの謎バグにハマってしまい断念・・・
他のプロジェクトではちゃんとSSR→SSGの切り替えが簡単にできてすげーー!!ってなりました