Vikeを試してみた

この記事は、[木更津高専 Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nit_kisarazu) 17日目の記事です。
←前 [Minio×Hono×Reactをやろうとした経験談を書いておこうと思ふ。](https://qiita.com/nairoki/items/54996e6241bbab1c2b84) by @nairoki 

Vikeとかいうフレームワークがなんか良さそうなので調べながら何かつくってみました

# Vikeの特徴
[Vike公式サイト](https://vike.dev/)にはこのような記述がある
![Flexible Your Stack, Your Choice](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/4c952d07-5f6c-84b7-e5d5-19d03aecb892.png)
要するに好きなフレームワークを自分で組み合わせられるということらしい。自由度が高いのは個人的に好き。
どのくらい自由かは公式プロジェクトジェネレーターの「Bâti」を使えばわかった
## 公式ジェネレーター Bâti
Bâtiを使うと簡単にVikeプロジェクトを始められるっぽい。

https://batijs.dev/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/0eb2961b-4c37-981b-df9c-913667ff82a1.png)

ここにある選択肢から自由に選べる（もちろんVueとMantineとかはエラーでる）
上に出てくるコマンド(`npm/yarn/pnpm/bun ~`)を使えば手元のPCで開発を始められる。
また、「Try me in Stackblitz」を押すと、選んだ構成がStackblitzで開かれ、ブラウザ上で簡単に試せる。

Presetを使うと手軽に設定できるらしく「Next.js」や「Nuxt」を選ぶとReactやVueを使ったそれっぽい構成になる。

[Telefunc](https://telefunc.com/) って何だろうと思ったらVike開発者が開発したサーバーサイドの処理をクライアントから呼び出せるやつらしい

# とりあえず何か作ってみる
なんか作ったほうが理解を深められると思うのでブログシステムでも作ってみる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/22f63a55-185c-c02e-90b7-d047934e52ae.png)
```
pnpm create bati --react --tailwindcss --shadcn-ui --lucia-auth --telefunc --hono --drizzle --biome
```

実行するとプロジェクトができる

**ファイル構造**
(sqlite.dbは`pnpm dev`した後にできた)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/21e7d7a5-82e4-352b-b7a7-5a2dff7c79b1.png)
**ページ**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/28925aeb-3286-ab96-02f3-4ae356062f9c.png)

デフォルトページの機能なんてカウンターだけかと思ったら、DBを使うTodoやTelefuncを使ったData Fetching、更にLuciaを使ったログインページまでもが生成されていた。これなら初心者でも安心？



## `+config.ts`
https://vike.dev/config

https://vike.dev/settings

レイアウトやタイトルなどの設定などができる。
例えばrootの`pages/+config.ts`はこんな感じ
```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オブジェクトから得られる。

https://vike.dev/pageContext

pageContextには現在のページに関する情報がある。
また、ミドルウェアを使って自分で好きなプロパティを設定できる。

```tsx:layouts/LayoutDefault.tsx
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`で簡単に実現できる。

https://vike.dev/guard

今回は投稿ページを例に実装する。
まず`pages/`以下に`+guard.ts`と`+Page.tsx`を作る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/520957f4-9feb-30cf-2ce8-caad42d4c9a8.png)
`+guard.ts`には以下のように書くだけ！
```ts:+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 };
```
`+Page.tsx`には適当に投稿ページを作る
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/a6a2f6a1-3c56-cb8c-6437-5639ec459075.png)


これだけで`/new-post`がログインユーザーのみアクセス可能になった。
## DBに追加操作をする
Telefuncを使ってサーバーサイドでDBを操作する。
```ts:Post.telefunc.ts
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);
}
```

呼び出し側はこう書くだけ
```ts:pages/new-post/+Page.tsx (一部)
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()`を使う

https://vike.dev/data

```ts:pages/index/+data.ts
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()`を使う

https://vike.dev/useData

```ts:pages/index/+Page.ts
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} />
      ))}
    </>
  );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/057af36c-707d-3298-2049-ad3202524ff1.png)


ブログっぽくなった

### パスパラメーターを使う
ユーザー用のページを`/users/<ユーザー名>`に置きたいときは、`pages/users/@username`のようにディレクトリを作成する。
同じように`+data.ts`や`+Page.tsx`を書く
```ts:pages/users/@username/+data.ts
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>>
```
```tsx:pages/users/@username/+Page.tsx
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} />
      ))}
    </>
  );
}

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/5da9b290-0dc2-ea80-0d86-b1c613e70155.png)

同じように投稿詳細ページも作る
`pages/posts/@id`下に`+data.ts`や`+Page.tsx`を書くと・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/357553/c51b777d-834a-5ea5-5561-8a94501f8e36.png)

できた！
ブログだ！！

# おわり
ほんとはSSR→SSGが簡単にできるんだぞってことを見せたかったんですが、Drizzleの謎バグにハマってしまい断念・・・
他のプロジェクトではちゃんとSSR→SSGの切り替えが簡単にできてすげーー！！ってなりました

https://github.com/naotiki/vike-blog