Fresh 2.0 Denoフレームワーク完全ガイド


Fresh 2.0は、Denoエコシステムにおける次世代Webフレームワークとして大きな注目を集めています。本記事では、Fresh 2.0の新機能からIslands Architectureの実践的な使い方、Deno Deployとの連携、パフォーマンス最適化まで、完全に解説します。

Fresh 2.0とは

Freshは、Deno向けに設計されたフルスタックWebフレームワークです。従来のSPA(Single Page Application)とは異なり、サーバーサイドレンダリング(SSR)を基本としながら、必要な部分だけクライアントサイドでインタラクティブにする「Islands Architecture」を採用しています。

Fresh 2.0の主要な特徴

  • ゼロビルドステップ: 開発時にバンドルやトランスパイルが不要
  • TypeScriptネイティブ: Denoの型安全性を最大限活用
  • Islands Architecture: 必要な部分だけハイドレーション
  • エッジ対応: Deno Deployでグローバル配信が容易
  • 軽量: JavaScriptの配信量を最小化

Fresh 2.0の新機能

Fresh 2.0では、開発体験とパフォーマンスが大幅に向上しました。

プラグインシステムの強化

Fresh 2.0では、プラグインシステムが完全に再設計されました。以下は、Tailwind CSSプラグインの使用例です。

// fresh.config.ts
import { defineConfig } from "$fresh/server.ts";
import tailwind from "$fresh/plugins/tailwind.ts";

export default defineConfig({
  plugins: [tailwind()],
});

改善されたルーティング

ファイルベースルーティングがより直感的になりました。

routes/
├── index.tsx          # /
├── about.tsx          # /about
├── blog/
│   ├── index.tsx     # /blog
│   ├── [slug].tsx    # /blog/:slug
│   └── _layout.tsx   # レイアウトコンポーネント
└── api/
    └── posts.ts      # /api/posts

ミドルウェアの改善

Fresh 2.0では、ミドルウェアの記述がより簡潔になりました。

// routes/_middleware.ts
import { FreshContext } from "$fresh/server.ts";

export async function handler(req: Request, ctx: FreshContext) {
  // 認証チェック
  const token = req.headers.get("authorization");

  if (!token && ctx.destination !== "route" || ctx.destination === "static") {
    return await ctx.next();
  }

  if (!token && new URL(req.url).pathname !== "/login") {
    return new Response("Unauthorized", { status: 401 });
  }

  ctx.state.user = await validateToken(token);
  return await ctx.next();
}

async function validateToken(token: string) {
  // トークン検証ロジック
  return { id: 1, name: "User" };
}

Islands Architectureの深堀り

Islands Architectureは、Freshの最大の特徴です。ページ全体をJavaScriptでハイドレートするのではなく、インタラクティブが必要な「島(Island)」だけを選択的にハイドレートします。

Islandコンポーネントの作成

// islands/Counter.tsx
import { Signal, useSignal } from "@preact/signals";

export default function Counter() {
  const count = useSignal(0);

  return (
    <div class="counter">
      <p>カウント: {count.value}</p>
      <button onClick={() => count.value++}>
        増やす
      </button>
      <button onClick={() => count.value--}>
        減らす
      </button>
    </div>
  );
}

静的ページでIslandを使用

// routes/index.tsx
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div>
      <h1>ようこそFresh 2.0へ</h1>
      <p>このページは完全に静的です。</p>

      {/* この部分だけインタラクティブ */}
      <Counter />

      <footer>
        <p>フッターも静的なHTMLです。</p>
      </footer>
    </div>
  );
}

この構造により、JavaScriptの配信量を最小限に抑えながら、必要な部分だけインタラクティブにできます。

Preactベースのコンポーネント設計

FreshはPreactをUIライブラリとして採用しています。Reactとほぼ同じAPIを持ちながら、はるかに軽量です。

Signalsによる状態管理

Fresh 2.0では、Preact Signalsが推奨される状態管理方法です。

// islands/TodoList.tsx
import { useSignal, useComputed } from "@preact/signals";

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

export default function TodoList() {
  const todos = useSignal<Todo[]>([]);
  const newTodo = useSignal("");

  const activeTodos = useComputed(() =>
    todos.value.filter(t => !t.completed)
  );

  const addTodo = () => {
    if (newTodo.value.trim()) {
      todos.value = [...todos.value, {
        id: Date.now(),
        text: newTodo.value,
        completed: false,
      }];
      newTodo.value = "";
    }
  };

  const toggleTodo = (id: number) => {
    todos.value = todos.value.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
  };

  return (
    <div class="todo-list">
      <h2>TODOリスト</h2>

      <div class="input-group">
        <input
          type="text"
          value={newTodo.value}
          onInput={(e) => newTodo.value = e.currentTarget.value}
          onKeyPress={(e) => e.key === "Enter" && addTodo()}
          placeholder="新しいTODOを入力"
        />
        <button onClick={addTodo}>追加</button>
      </div>

      <p>残り: {activeTodos.value.length}</p>

      <ul>
        {todos.value.map(todo => (
          <li key={todo.id} class={todo.completed ? "completed" : ""}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

コンポーネント間のデータ受け渡し

// routes/posts/[id].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
import CommentList from "../../islands/CommentList.tsx";

interface Post {
  id: string;
  title: string;
  content: string;
}

export const handler: Handlers<Post> = {
  async GET(_req, ctx) {
    const post = await fetchPost(ctx.params.id);
    if (!post) {
      return ctx.renderNotFound();
    }
    return ctx.render(post);
  },
};

export default function PostPage({ data }: PageProps<Post>) {
  return (
    <article>
      <h1>{data.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.content }} />

      {/* Islandにサーバーサイドのデータを渡す */}
      <CommentList postId={data.id} />
    </article>
  );
}

async function fetchPost(id: string): Promise<Post | null> {
  // データベースから取得
  return {
    id,
    title: "サンプル記事",
    content: "<p>本文...</p>",
  };
}

Deno Deployとの連携

Fresh 2.0は、Deno Deployとシームレスに統合されており、数分でグローバル配信が可能です。

デプロイ設定

// deno.json
{
  "tasks": {
    "dev": "deno run -A --watch=static/,routes/ dev.ts",
    "build": "deno run -A dev.ts build",
    "preview": "deno run -A main.ts",
    "update": "deno run -A -r https://fresh.deno.dev/update ."
  },
  "imports": {
    "$fresh/": "https://deno.land/x/fresh@2.0.0-alpha.19/",
    "preact": "https://esm.sh/preact@10.19.2",
    "@preact/signals": "https://esm.sh/*@preact/signals@1.2.1"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

GitHub Actionsでの自動デプロイ

# .github/workflows/deploy.yml
name: Deploy to Deno Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v3

      - uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x

      - name: Deploy to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: "my-fresh-app"
          entrypoint: "main.ts"

エッジでのデータフェッチ

Deno Deployのエッジランタイムを活用した例です。

// routes/api/location.ts
import { Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(req) {
    // クライアントの地理的位置情報を取得
    const location = req.headers.get("cf-ipcountry") || "Unknown";

    // エッジでKVからデータを取得
    const kv = await Deno.openKv();
    const regionData = await kv.get(["regions", location]);

    return new Response(JSON.stringify({
      location,
      data: regionData.value,
    }), {
      headers: { "content-type": "application/json" },
    });
  },
};

パフォーマンス最適化

Fresh 2.0では、デフォルトで高いパフォーマンスが得られますが、さらに最適化する方法があります。

画像の最適化

// routes/gallery.tsx
import { asset } from "$fresh/runtime.ts";

export default function Gallery() {
  return (
    <div class="gallery">
      {/* 静的アセットの最適化 */}
      <img
        src={asset("/images/hero.jpg")}
        alt="Hero"
        width={1200}
        height={630}
        loading="lazy"
      />
    </div>
  );
}

プリフェッチの実装

// islands/NavigationLink.tsx
import { useEffect } from "preact/hooks";

interface Props {
  href: string;
  children: preact.ComponentChildren;
}

export default function NavigationLink({ href, children }: Props) {
  useEffect(() => {
    // ホバー時にプリフェッチ
    const link = document.querySelector(`a[href="${href}"]`);
    if (!link) return;

    const prefetch = () => {
      const linkElement = document.createElement("link");
      linkElement.rel = "prefetch";
      linkElement.href = href;
      document.head.appendChild(linkElement);
    };

    link.addEventListener("mouseenter", prefetch, { once: true });
    return () => link.removeEventListener("mouseenter", prefetch);
  }, [href]);

  return <a href={href}>{children}</a>;
}

Deno KVでキャッシング

// routes/api/posts/[id].ts
import { Handlers } from "$fresh/server.ts";

const kv = await Deno.openKv();

export const handler: Handlers = {
  async GET(_req, ctx) {
    const postId = ctx.params.id;

    // キャッシュを確認
    const cached = await kv.get(["posts", postId]);
    if (cached.value) {
      return new Response(JSON.stringify(cached.value), {
        headers: {
          "content-type": "application/json",
          "x-cache": "HIT",
        },
      });
    }

    // DBから取得
    const post = await fetchPostFromDB(postId);

    // キャッシュに保存(1時間)
    await kv.set(["posts", postId], post, {
      expireIn: 3600000,
    });

    return new Response(JSON.stringify(post), {
      headers: {
        "content-type": "application/json",
        "x-cache": "MISS",
      },
    });
  },
};

async function fetchPostFromDB(id: string) {
  // 実際のDB処理
  return { id, title: "Post", content: "..." };
}

まとめ

Fresh 2.0は、Denoエコシステムにおける強力なWebフレームワークです。Islands Architectureによる最適なJavaScript配信、Preactの軽量性、Deno Deployとのシームレスな統合により、高速で保守しやすいWebアプリケーションを構築できます。

特に以下のプロジェクトに適しています。

  • コンテンツ中心のサイト: ブログ、ドキュメントサイト
  • Eコマース: 高速なページロードが重要
  • ダッシュボード: 部分的なインタラクティビティが必要
  • グローバルアプリ: エッジデプロイが有効

Fresh 2.0とDenoで、次世代のWeb開発を体験してみてください。