2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

木更津高専Advent Calendar 2024

Day 22

Vikeを試してみた

Last updated at Posted at 2024-12-22

この記事は、木更津高専 Advent Calendar 2024 17日目の記事です。
←前 Minio×Hono×Reactをやろうとした経験談を書いておこうと思ふ。 by @nairoki

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

Vikeの特徴

Vike公式サイトにはこのような記述がある
Flexible Your Stack, Your Choice
要するに好きなフレームワークを自分で組み合わせられるということらしい。自由度が高いのは個人的に好き。
どのくらい自由かは公式プロジェクトジェネレーターの「Bâti」を使えばわかった

公式ジェネレーター Bâti

Bâtiを使うと簡単にVikeプロジェクトを始められるっぽい。

image.png

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

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

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

とりあえず何か作ってみる

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

image.png

pnpm create bati --react --tailwindcss --shadcn-ui --lucia-auth --telefunc --hono --drizzle --biome

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

ファイル構造
(sqlite.dbはpnpm devした後にできた)
image.png
ページ
image.png

デフォルトページの機能なんてカウンターだけかと思ったら、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には現在のページに関する情報がある。
また、ミドルウェアを使って自分で好きなプロパティを設定できる。

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で簡単に実現できる。

今回は投稿ページを例に実装する。
まずpages/以下に+guard.ts+Page.tsxを作る
image.png
+guard.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

これだけで/new-postがログインユーザーのみアクセス可能になった。

DBに追加操作をする

Telefuncを使ってサーバーサイドでDBを操作する。

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);
}

呼び出し側はこう書くだけ

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()を使う

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()を使う

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

ブログっぽくなった

パスパラメーターを使う

ユーザー用のページを/users/<ユーザー名>に置きたいときは、pages/users/@usernameのようにディレクトリを作成する。
同じように+data.ts+Page.tsxを書く

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>>
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

同じように投稿詳細ページも作る
pages/posts/@id下に+data.ts+Page.tsxを書くと・・・
image.png

できた!
ブログだ!!

おわり

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?