最終更新:

Deno Fresh 2完全ガイド: Island Architectureで高速Webアプリ構築


Deno Fresh 2完全ガイド: Island Architectureで高速Webアプリ構築

Deno Fresh 2は、Island Architectureを採用した次世代のWebフレームワークです。従来のSPAとは異なり、必要な部分だけをインタラクティブにすることで、圧倒的なパフォーマンスを実現します。この記事では、Fresh 2の特徴から実践的な開発手法まで完全解説します。

Fresh 2の特徴

1. Island Architecture

Island Architectureは、ページの大部分を静的HTMLとして配信し、動的なインタラクティブ要素(Island)だけをクライアントサイドJavaScriptで動作させる設計パターンです。

// routes/index.tsx
import { define } from "$fresh/server.ts";
import Counter from "../islands/Counter.tsx";

export default define.page(function Home() {
  return (
    <div class="container">
      <h1>Welcome to Fresh 2</h1>
      <p>This is static content - no JS required!</p>

      {/* この部分だけがインタラクティブなIsland */}
      <Counter start={0} />
    </div>
  );
});
// islands/Counter.tsx
import { signal } from "@preact/signals";

export default function Counter({ start }: { start: number }) {
  const count = signal(start);

  return (
    <div class="counter">
      <p>Count: {count.value}</p>
      <button onClick={() => count.value++}>+1</button>
      <button onClick={() => count.value--}>-1</button>
    </div>
  );
}

2. ゼロJavaScriptビルドステップ

Fresh 2は、ビルドステップなしで動作します。TypeScriptは実行時に自動的にトランスパイルされ、開発体験が大幅に向上します。

// deno.json
{
  "tasks": {
    "dev": "deno run -A --watch=static/,routes/ dev.ts",
    "preview": "deno run -A main.ts",
    "build": "deno run -A dev.ts build",
    "start": "deno run -A main.ts"
  },
  "imports": {
    "$fresh/": "https://deno.land/x/fresh@2.0.0-alpha.18/",
    "preact": "https://esm.sh/preact@10.19.6",
    "@preact/signals": "https://esm.sh/@preact/signals@1.2.3"
  }
}

3. Preact Signalsによる状態管理

Fresh 2は、Preact Signalsを標準の状態管理システムとして採用しています。リアクティブでパフォーマンスが高く、学習コストも低いのが特徴です。

// islands/TodoList.tsx
import { signal, computed } from "@preact/signals";

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

const todos = signal<Todo[]>([
  { id: 1, text: "Learn Fresh 2", completed: true },
  { id: 2, text: "Build an app", completed: false },
]);

const activeTodos = computed(() =>
  todos.value.filter(todo => !todo.completed)
);

const completedTodos = computed(() =>
  todos.value.filter(todo => todo.completed)
);

export default function TodoList() {
  const addTodo = (text: string) => {
    todos.value = [
      ...todos.value,
      { id: Date.now(), text, completed: false }
    ];
  };

  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 List ({activeTodos.value.length} active)</h2>
      <TodoInput onAdd={addTodo} />
      <ul>
        {todos.value.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={() => toggleTodo(todo.id)}
          />
        ))}
      </ul>
      <div class="stats">
        <p>Completed: {completedTodos.value.length}</p>
      </div>
    </div>
  );
}

ルーティングシステム

Fresh 2は、ファイルシステムベースのルーティングを採用しています。

基本的なルート定義

// routes/index.tsx - /
// routes/about.tsx - /about
// routes/blog/[slug].tsx - /blog/:slug
// routes/api/users/[id].ts - /api/users/:id

// routes/blog/[slug].tsx
import { define } from "$fresh/server.ts";

export const handler = define.handlers({
  async GET(ctx) {
    const { slug } = ctx.params;
    const post = await loadBlogPost(slug);

    if (!post) {
      return ctx.renderNotFound();
    }

    return ctx.render({ post });
  },
});

export default define.page<typeof handler>(function BlogPost({ data }) {
  const { post } = data;

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
});

APIルート

// routes/api/todos.ts
import { define } from "$fresh/server.ts";

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

const todos: Todo[] = [];

export const handler = define.handlers({
  GET(_req) {
    return Response.json(todos);
  },

  async POST(req) {
    const { text } = await req.json();
    const todo: Todo = {
      id: Date.now(),
      text,
      completed: false,
    };
    todos.push(todo);
    return Response.json(todo, { status: 201 });
  },

  async PUT(req) {
    const { id, completed } = await req.json();
    const todo = todos.find(t => t.id === id);
    if (!todo) {
      return new Response("Not Found", { status: 404 });
    }
    todo.completed = completed;
    return Response.json(todo);
  },

  DELETE(req) {
    const url = new URL(req.url);
    const id = Number(url.searchParams.get("id"));
    const index = todos.findIndex(t => t.id === id);
    if (index === -1) {
      return new Response("Not Found", { status: 404 });
    }
    todos.splice(index, 1);
    return new Response(null, { status: 204 });
  },
});

データフェッチングとSSR

Fresh 2は、サーバーサイドレンダリングを標準でサポートしています。

ページでのデータフェッチング

// routes/users/index.tsx
import { define } from "$fresh/server.ts";

interface User {
  id: number;
  name: string;
  email: string;
}

export const handler = define.handlers({
  async GET(ctx) {
    const response = await fetch("https://api.example.com/users");
    const users: User[] = await response.json();

    return ctx.render({ users });
  },
});

export default define.page<typeof handler>(function UsersPage({ data }) {
  const { users } = data;

  return (
    <div class="users-page">
      <h1>Users</h1>
      <ul class="user-list">
        {users.map(user => (
          <li key={user.id}>
            <a href={`/users/${user.id}`}>{user.name}</a>
            <span>{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
});

ミドルウェア

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

export const handler = define.middleware([
  // ロギングミドルウェア
  async function logger(ctx) {
    const start = Date.now();
    const response = await ctx.next();
    const duration = Date.now() - start;
    console.log(`${ctx.url.pathname} - ${duration}ms`);
    return response;
  },

  // 認証ミドルウェア
  async function auth(ctx) {
    const session = ctx.state.session;
    if (!session && ctx.url.pathname.startsWith("/dashboard")) {
      return ctx.redirect("/login");
    }
    return await ctx.next();
  },
]);

Islandの高度な使い方

親子間でのデータ共有

// lib/store.ts
import { signal } from "@preact/signals";

export interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

export const cart = signal<CartItem[]>([]);

export const addToCart = (item: Omit<CartItem, "quantity">) => {
  const existing = cart.value.find(i => i.id === item.id);
  if (existing) {
    cart.value = cart.value.map(i =>
      i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
    );
  } else {
    cart.value = [...cart.value, { ...item, quantity: 1 }];
  }
};

export const removeFromCart = (id: number) => {
  cart.value = cart.value.filter(i => i.id !== id);
};

export const cartTotal = computed(() =>
  cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
);
// islands/CartButton.tsx
import { cart } from "../lib/store.ts";

export default function CartButton() {
  return (
    <button class="cart-button">
      Cart ({cart.value.length})
    </button>
  );
}
// islands/ProductCard.tsx
import { addToCart } from "../lib/store.ts";

interface Product {
  id: number;
  name: string;
  price: number;
  image: string;
}

export default function ProductCard({ product }: { product: Product }) {
  return (
    <div class="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => addToCart(product)}>
        Add to Cart
      </button>
    </div>
  );
}

プロダクション向け最適化

1. 静的ファイルの最適化

// static/images/ 配下の画像は自動的に最適化されます
// routes/index.tsx
export default define.page(function Home() {
  return (
    <div>
      <img
        src="/images/hero.jpg"
        alt="Hero"
        width={1200}
        height={630}
        loading="lazy"
      />
    </div>
  );
});

2. CSS最適化

// routes/_app.tsx
import { define } from "$fresh/server.ts";

export default define.page(function App({ Component }) {
  return (
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>My Fresh App</title>
        <link rel="stylesheet" href="/styles/main.css" />
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
});

3. デプロイ

# Deno Deployへのデプロイ
deno task build
deployctl deploy --project=my-fresh-app --prod

# Dockerでデプロイ
# Dockerfile
FROM denoland/deno:alpine

WORKDIR /app
COPY . .

RUN deno cache main.ts

EXPOSE 8000
CMD ["run", "-A", "main.ts"]

まとめ

Deno Fresh 2は、Island Architectureという革新的なアプローチにより、高速でモダンなWebアプリケーション開発を実現します。主な利点は以下の通りです。

  • 高速なパフォーマンス: 必要最小限のJavaScriptのみをクライアントに送信
  • 優れた開発体験: ビルドステップ不要、TypeScript標準サポート
  • シンプルな状態管理: Preact Signalsによる直感的なリアクティブプログラミング
  • 標準準拠: Web標準APIを積極的に活用

Fresh 2を使って、次世代のWebアプリケーション開発を始めましょう。