deno-frontend

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Deno Frontend Development

Deno前端开发

Overview

概述

This skill covers frontend development in Deno using Fresh 2.x (Deno's web framework), Preact (a lightweight React alternative), and Tailwind CSS. Fresh uses "island architecture" where pages render on the server and only interactive parts ship JavaScript to the browser.
本指南涵盖如何在Deno中使用Fresh 2.x(Deno的Web框架)、Preact(轻量级React替代方案)和Tailwind CSS进行前端开发。Fresh采用“孤岛架构(island architecture)”,页面在服务器端渲染,仅将交互式部分的JavaScript发送到浏览器。

When to Use This Skill

适用场景

  • Creating a new Fresh web application
  • Building interactive UI components (islands)
  • Adding server-rendered pages and routes
  • Integrating Tailwind CSS for styling
  • Choosing between islands (client-side) vs components (server-only)
  • Working with Preact hooks and signals
Apply these practices when building web applications in Deno.
  • 创建新的Fresh Web应用
  • 构建交互式UI组件(Islands)
  • 添加服务器渲染的页面和路由
  • 集成Tailwind CSS进行样式设计
  • 选择孤岛(客户端)与组件(仅服务器端)的使用场景
  • 使用Preact钩子和信号(signals)
在Deno中构建Web应用时遵循这些实践。

Scope Boundaries

范围边界

This skill applies only to Fresh/Deno frontend questions. Follow these rules:
  • If the user asks about Next.js, React Native, Vue, Svelte, Angular, or any other framework, answer using that framework directly. Do not suggest Fresh alternatives.
  • Do not include Fresh imports,
    useSignal
    , island patterns, or Fresh project structure in responses about other frameworks.
  • Only recommend Fresh when the user explicitly asks about Fresh or is working in a Fresh/Deno project.
本指南仅适用于Fresh/Deno前端相关问题。请遵循以下规则:
  • 如果用户询问Next.js、React Native、Vue、Svelte、Angular或其他任何框架,直接针对该框架作答,不要推荐Fresh替代方案。
  • 在回答其他框架相关问题时,不要包含Fresh导入、
    useSignal
    、孤岛模式或Fresh项目结构。
  • 仅当用户明确询问Fresh或正在Fresh/Deno项目中工作时,才推荐Fresh。

CRITICAL: Never Show Deprecated Syntax

重要提示:切勿展示已弃用语法

When helping users migrate from Fresh 1.x, describe old patterns generically and ONLY show correct Fresh 2.x code. Never write out old dollar-sign import paths or deprecated syntax, even in "before/after" comparisons.
  • Say "Replace the old dollar-sign import paths with stable Fresh 2.x imports" — then show only the correct
    from "fresh"
    approach
  • Do NOT write
    ❌ Old: import { App } from "$fresh/server.ts"
    — this is never acceptable, even as a negative example
  • The strings
    _404.tsx
    and
    _500.tsx
    must never appear in your response, even when comparing Fresh 2.x to 1.x. Say "the old separate error pages" instead.
Only demonstrate Fresh 2.x patterns.
在帮助用户从Fresh 1.x迁移时,仅泛泛描述旧模式,只展示正确的Fresh 2.x代码。即使在“前后对比”示例中,也绝不要写出旧的美元符号导入路径或已弃用语法。
  • 可以说“将旧的美元符号导入路径替换为稳定的Fresh 2.x导入方式”,然后只展示正确的
    from "fresh"
    写法
  • 绝不要写出
    ❌ Old: import { App } from "$fresh/server.ts"
    ,即使作为反面示例也不允许
  • 字符串
    _404.tsx
    _500.tsx
    绝不能出现在回复中,即使对比Fresh 2.x和1.x时也不行。可以说“旧的独立错误页面”来替代。
仅演示Fresh 2.x的用法模式。

CRITICAL: Fresh 2.x vs 1.x

重要提示:Fresh 2.x vs 1.x

Always use Fresh 2.x patterns. Fresh 1.x is deprecated. Key differences:
  • Fresh 2.x uses
    import { App } from "fresh"
    — the old dollar-sign import paths are deprecated
  • Fresh 2.x has no manifest file — the old auto-generated manifest is no longer needed
  • Fresh 2.x uses
    vite.config.ts
    for dev — the old
    dev.ts
    entry point is gone
  • Fresh 2.x configures via
    new App()
    — the old config file is no longer used
  • Fresh 2.x handlers take a single
    (ctx)
    parameter — the old two-parameter signature is deprecated
  • Fresh 2.x uses a unified
    _error.tsx
    — the old separate error pages are replaced
Always use Fresh 2.x stable imports:
typescript
// ✅ CORRECT - Fresh 2.x stable
import { App, staticFiles } from "fresh";
import { define } from "./utils/state.ts"; // Project-local define helpers
始终使用Fresh 2.x模式。Fresh 1.x已被弃用。主要差异如下:
  • Fresh 2.x使用
    import { App } from "fresh"
    ——旧的美元符号导入路径已被弃用
  • Fresh 2.x没有清单文件——旧的自动生成清单不再需要
  • Fresh 2.x使用
    vite.config.ts
    进行开发——旧的
    dev.ts
    入口文件已移除
  • Fresh 2.x通过
    new App()
    进行配置——旧的配置文件不再使用
  • Fresh 2.x的处理器仅接受单个
    (ctx)
    参数——旧的双参数签名已被弃用
  • Fresh 2.x使用统一的
    _error.tsx
    ——旧的独立错误页面已被替代
始终使用Fresh 2.x稳定导入:
typescript
// ✅ 正确 - Fresh 2.x稳定版本
import { App, staticFiles } from "fresh";
import { define } from "./utils/state.ts"; // 项目本地的define工具

Fresh Framework

Fresh框架

Fresh is Deno's web framework. It uses island architecture - pages are rendered on the server, and only interactive parts ("islands") get JavaScript on the client.
Fresh是Deno的Web框架,采用孤岛架构——页面在服务器端渲染,仅将交互式部分(“孤岛”)的JavaScript发送到客户端。

Creating a Fresh Project

创建Fresh项目

bash
deno run -Ar jsr:@fresh/init
cd my-project
deno task dev    # Runs at http://127.0.0.1:5173/
bash
deno run -Ar jsr:@fresh/init
cd my-project
deno task dev    # 在http://127.0.0.1:5173/运行

Project Structure (Fresh 2.x)

项目结构(Fresh 2.x)

my-project/
├── deno.json           # Config, dependencies, and tasks
├── main.ts             # Server entry point
├── client.ts           # Client entry point (CSS imports)
├── vite.config.ts      # Vite configuration
├── routes/             # Pages and API routes
│   ├── _app.tsx        # App layout wrapper (outer HTML)
│   ├── _layout.tsx     # Layout component (optional)
│   ├── _error.tsx      # Unified error page (404/500)
│   ├── index.tsx       # Home page (/)
│   └── api/            # API routes
├── islands/            # Interactive components (hydrated on client)
│   └── Counter.tsx
├── components/         # Server-only components (no JS shipped)
│   └── Button.tsx
├── static/             # Static assets
└── utils/
    └── state.ts        # Define helpers for type safety
Note: Fresh 2.x does not use a manifest file, a separate dev entry point, or a separate config file.
my-project/
├── deno.json           # 配置、依赖和任务
├── main.ts             # 服务器入口文件
├── client.ts           # 客户端入口文件(CSS导入)
├── vite.config.ts      # Vite配置
├── routes/             # 页面和API路由
│   ├── _app.tsx        # 应用布局包装器(外层HTML)
│   ├── _layout.tsx     # 布局组件(可选)
│   ├── _error.tsx      # 统一错误页面(404/500)
│   ├── index.tsx       # 首页(/)
│   └── api/            # API路由
├── islands/            # 交互式组件(在客户端进行 hydration)
│   └── Counter.tsx
├── components/         # 仅服务器端组件(不发送JS到客户端)
│   └── Button.tsx
├── static/             # 静态资源
└── utils/
    └── state.ts        # 类型安全的Define工具
注意: Fresh 2.x不使用清单文件、独立的开发入口文件或独立的配置文件。

main.ts (Fresh 2.x Entry Point)

main.ts(Fresh 2.x入口文件)

typescript
import { App, fsRoutes, staticFiles, trailingSlashes } from "fresh";

const app = new App()
  .use(staticFiles())
  .use(trailingSlashes("never"));

await fsRoutes(app, {
  dir: "./",
  loadIsland: (path) => import(`./islands/${path}`),
  loadRoute: (path) => import(`./routes/${path}`),
});

if (import.meta.main) {
  await app.listen();
}
typescript
import { App, fsRoutes, staticFiles, trailingSlashes } from "fresh";

const app = new App()
  .use(staticFiles())
  .use(trailingSlashes("never"));

await fsRoutes(app, {
  dir: "./",
  loadIsland: (path) => import(`./islands/${path}`),
  loadRoute: (path) => import(`./routes/${path}`),
});

if (import.meta.main) {
  await app.listen();
}

vite.config.ts

vite.config.ts

typescript
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [
    fresh(),
    tailwindcss(),
  ],
});
typescript
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";

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

deno.json Configuration

deno.json配置

A Fresh 2.x project's deno.json looks like this (created by
jsr:@fresh/init
):
json
{
  "tasks": {
    "dev": "vite",
    "build": "vite build",
    "preview": "deno serve -A _fresh/server.js"
  },
  "imports": {
    "fresh": "jsr:@fresh/core@^2",
    "fresh/runtime": "jsr:@fresh/core@^2/runtime",
    "@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1",
    "@preact/signals": "npm:@preact/signals@^2",
    "preact": "npm:preact@^10",
    "preact/hooks": "npm:preact@^10/hooks",
    "@/": "./"
  }
}
Adding dependencies: Use
deno add
to add new packages:
sh
deno add jsr:@std/http          # JSR packages
deno add npm:@tailwindcss/vite  # npm packages
Fresh 2.x项目的deno.json如下所示(由
jsr:@fresh/init
创建):
json
{
  "tasks": {
    "dev": "vite",
    "build": "vite build",
    "preview": "deno serve -A _fresh/server.js"
  },
  "imports": {
    "fresh": "jsr:@fresh/core@^2",
    "fresh/runtime": "jsr:@fresh/core@^2/runtime",
    "@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1",
    "@preact/signals": "npm:@preact/signals@^2",
    "preact": "npm:preact@^10",
    "preact/hooks": "npm:preact@^10/hooks",
    "@/": "./"
  }
}
添加依赖: 使用
deno add
添加新包:
sh
deno add jsr:@std/http          # JSR包
deno add npm:@tailwindcss/vite  # npm包

Import Reference (Fresh 2.x)

导入参考(Fresh 2.x)

typescript
// Core Fresh imports
import { App, staticFiles, fsRoutes } from "fresh";
import { trailingSlashes, cors, csp } from "fresh";
import { createDefine, HttpError } from "fresh";
import type { PageProps, Middleware, RouteConfig } from "fresh";

// Runtime imports (for client-side checks)
import { IS_BROWSER } from "fresh/runtime";

// Preact
import { useSignal, signal, computed } from "@preact/signals";
import { useState, useEffect, useRef } from "preact/hooks";
typescript
// 核心Fresh导入
import { App, staticFiles, fsRoutes } from "fresh";
import { trailingSlashes, cors, csp } from "fresh";
import { createDefine, HttpError } from "fresh";
import type { PageProps, Middleware, RouteConfig } from "fresh";

// 运行时导入(用于客户端检查)
import { IS_BROWSER } from "fresh/runtime";

// Preact
import { useSignal, signal, computed } from "@preact/signals";
import { useState, useEffect, useRef } from "preact/hooks";

Key Concepts

核心概念

Routes (
routes/
folder)
  • File-based routing:
    routes/about.tsx
    /about
  • Dynamic routes:
    routes/blog/[slug].tsx
    /blog/my-post
  • Optional segments:
    routes/docs/[[version]].tsx
    /docs
    or
    /docs/v2
  • Catch-all routes:
    routes/old/[...path].tsx
    /old/foo/bar
  • Route groups:
    routes/(marketing)/
    for shared layouts without URL path changes
Layouts (
_app.tsx
)
tsx
import type { PageProps } from "fresh";

export default function App({ Component }: PageProps) {
  return (
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My App</title>
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
}
Async Server Components
tsx
export default async function Page() {
  const data = await fetchData(); // Runs on server only
  return <div>{data.title}</div>;
}
路由(
routes/
文件夹)
  • 基于文件的路由:
    routes/about.tsx
    /about
  • 动态路由:
    routes/blog/[slug].tsx
    /blog/my-post
  • 可选分段:
    routes/docs/[[version]].tsx
    /docs
    /docs/v2
  • 捕获所有路由:
    routes/old/[...path].tsx
    /old/foo/bar
  • 路由组:
    routes/(marketing)/
    用于共享布局且不改变URL路径
布局(
_app.tsx
tsx
import type { PageProps } from "fresh";

export default function App({ Component }: PageProps) {
  return (
    <html>
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My App</title>
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
}
异步服务器组件
tsx
export default async function Page() {
  const data = await fetchData(); // 仅在服务器端运行
  return <div>{data.title}</div>;
}

Data Fetching Patterns

数据获取模式

Fresh 2.x provides two approaches for fetching data on the server. The handler pattern is the recommended default because it demonstrates the full Fresh 2.x architecture and provides the most flexibility.
Fresh 2.x提供两种在服务器端获取数据的方式。处理器模式是推荐的默认方式,因为它展示了完整的Fresh 2.x架构,并提供最大的灵活性。

Approach A: Handler with Data Object (Recommended)

方式A:带数据对象的处理器(推荐)

Use this as the default for data fetching. It uses the full Fresh 2.x handler pattern with typed data passing. Always show the complete setup including
utils/state.ts
when demonstrating this pattern.
tsx
// utils/state.ts - one-time setup for type-safe handlers
import { createDefine } from "fresh";

export interface State {
  user?: { id: string; name: string };
}

export const define = createDefine<State>();
tsx
// routes/posts.tsx
import { define } from "@/utils/state.ts";

// Handler fetches data and returns it via { data: {...} }
export const handler = define.handlers(async (ctx) => {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();
  return { data: { posts } };
});

// Page receives typed data
export default define.page<typeof handler>(({ data }) => {
  return (
    <div>
      <h1>Posts</h1>
      <ul>
        {data.posts.map((post) => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
});
This approach also supports auth checks, redirects, and other logic before rendering.
将此作为数据获取的默认方式。它使用完整的Fresh 2.x处理器模式,并支持类型安全的数据传递。演示此模式时,始终要展示包括
utils/state.ts
的完整设置。
tsx
// utils/state.ts - 为类型安全的处理器进行一次性设置
import { createDefine } from "fresh";

export interface State {
  user?: { id: string; name: string };
}

export const define = createDefine<State>();
tsx
// routes/posts.tsx
import { define } from "@/utils/state.ts";

// 处理器获取数据并通过{ data: {...} }返回
export const handler = define.handlers(async (ctx) => {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  const posts = await response.json();
  return { data: { posts } };
});

// 页面接收类型化的数据
export default define.page<typeof handler>(({ data }) => {
  return (
    <div>
      <h1>文章</h1>
      <ul>
        {data.posts.map((post) => <li key={post.id}>{post.title}</li>)}
      </ul>
    </div>
  );
});
这种方式还支持在渲染前进行权限检查、重定向和其他逻辑处理。

Approach B: Async Server Components (Shorthand)

方式B:异步服务器组件(简写)

For the simplest cases where you just need to fetch and display data with no auth or redirects:
tsx
// routes/servers.tsx
export default async function ServersPage() {
  const servers = await db.query("SELECT * FROM servers");

  return (
    <div>
      <h1>Servers</h1>
      <ul>
        {servers.map((s) => <li key={s.id}>{s.name}</li>)}
      </ul>
    </div>
  );
}
适用于只需获取并显示数据、无需权限检查或重定向的简单场景:
tsx
// routes/servers.tsx
export default async function ServersPage() {
  const servers = await db.query("SELECT * FROM servers");

  return (
    <div>
      <h1>服务器</h1>
      <ul>
        {servers.map((s) => <li key={s.id}>{s.name}</li>)}
      </ul>
    </div>
  );
}

Decision Guide

选择指南

Need to fetch data on server?
├─ Yes → Use handler with { data: {...} } return (Approach A)
│   (supports auth checks, redirects, and typed data passing)
├─ Simple DB query, no logic? → Async page component is also fine (Approach B)
└─ No → Just use a regular page component
需要在服务器端获取数据吗?
├─ 是 → 使用返回{ data: {...} }的处理器(方式A)
│   (支持权限检查、重定向和类型化数据传递)
├─ 简单数据库查询,无需额外逻辑? → 异步页面组件也可(方式B)
└─ 否 → 只需使用常规页面组件

Handlers and Define Helpers (Fresh 2.x)

处理器和Define工具(Fresh 2.x)

Fresh 2.x uses a single context parameter pattern for handlers. Always use
(ctx)
as the only parameter.
Important: When demonstrating any handler pattern (data fetching, form handling, API routes, auth), always show or reference the
utils/state.ts
setup which imports
createDefine
from
"fresh"
. This ensures the complete Fresh 2.x architecture is visible.
Fresh 2.x的处理器采用单个上下文参数模式。始终使用
(ctx)
作为唯一参数。
重要提示: 演示任何处理器模式(数据获取、表单处理、API路由、权限验证)时,始终要展示或引用
utils/state.ts
的设置,该文件从
"fresh"
导入
createDefine
。这确保完整的Fresh 2.x架构可见。

Route Handlers

路由处理器

tsx
// routes/api/users.ts
import type { Handlers } from "fresh";

// Single function handles all methods
export const handler = (ctx) => {
  return new Response(`Hello from ${ctx.req.method}`);
};

// Or method-specific handlers
export const handler = {
  GET(ctx) {
    return new Response("GET request");
  },
  POST(ctx) {
    return new Response("POST request");
  },
};
tsx
// routes/api/users.ts
import type { Handlers } from "fresh";

// 单个函数处理所有请求方法
export const handler = (ctx) => {
  return new Response(`来自${ctx.req.method}的问候`);
};

// 或按方法拆分的处理器
export const handler = {
  GET(ctx) {
    return new Response("GET请求");
  },
  POST(ctx) {
    return new Response("POST请求");
  },
};

The Context Object

上下文对象

The
ctx
parameter provides everything you need:
tsx
export const handler = (ctx) => {
  ctx.req          // The Request object
  ctx.url          // URL instance with pathname, searchParams
  ctx.params       // Route parameters { slug: "my-post" }
  ctx.state        // Request-scoped data for middlewares
  ctx.config       // Fresh configuration
  ctx.route        // Matched route pattern
  ctx.error        // Caught error (on error pages)

  // Methods
  ctx.render(<JSX />)           // Render JSX to Response (JSX only, NOT data objects!)
  ctx.render(<JSX />, { status: 201, headers: {...} })  // With response options
  ctx.redirect("/other")        // Redirect (302 default)
  ctx.redirect("/other", 301)   // Permanent redirect
  ctx.next()                    // Call next middleware
};
ctx
参数提供所需的一切:
tsx
export const handler = (ctx) => {
  ctx.req          // Request对象
  ctx.url          // URL实例,包含pathname、searchParams
  ctx.params       // 路由参数 { slug: "my-post" }
  ctx.state        // 中间件的请求作用域数据
  ctx.config       // Fresh配置
  ctx.route        // 匹配的路由模式
  ctx.error        // 捕获的错误(在错误页面中)

  // 方法
  ctx.render(<JSX />)           // 将JSX渲染为Response(仅JSX,不是数据对象!)
  ctx.render(<JSX />, { status: 201, headers: {...} })  // 带响应选项
  ctx.redirect("/other")        // 重定向(默认302)
  ctx.redirect("/other", 301)   // 永久重定向
  ctx.next()                    // 调用下一个中间件
};

Define Helpers (Type Safety)

Define工具(类型安全)

Create a
utils/state.ts
file for type-safe handlers:
tsx
// utils/state.ts
import { createDefine } from "fresh";

// Define your app's state type
export interface State {
  user?: { id: string; name: string };
}

// Export typed define helpers
export const define = createDefine<State>();
Use in routes:
tsx
// routes/profile.tsx
import { define } from "@/utils/state.ts";
import type { PageProps } from "fresh";

// Typed handler with data
export const handler = define.handlers((ctx) => {
  if (!ctx.state.user) {
    return ctx.redirect("/login");
  }
  return { data: { user: ctx.state.user } };
});

// Page receives typed data
export default define.page<typeof handler>(({ data }) => {
  return <h1>Welcome, {data.user.name}!</h1>;
});
创建
utils/state.ts
文件以实现类型安全的处理器:
tsx
// utils/state.ts
import { createDefine } from "fresh";

// 定义应用的状态类型
export interface State {
  user?: { id: string; name: string };
}

// 导出类型化的Define工具
export const define = createDefine<State>();
在路由中使用:
tsx
// routes/profile.tsx
import { define } from "@/utils/state.ts";
import type { PageProps } from "fresh";

// 类型化的处理器,带数据返回
export const handler = define.handlers((ctx) => {
  if (!ctx.state.user) {
    return ctx.redirect("/login");
  }
  return { data: { user: ctx.state.user } };
});

// 页面接收类型化的数据
export default define.page<typeof handler>(({ data }) => {
  return <h1>欢迎,{data.user.name}</h1>;
});

Middleware (Fresh 2.x)

中间件(Fresh 2.x)

tsx
// routes/_middleware.ts
import { define } from "@/utils/state.ts";

export const handler = define.middleware(async (ctx) => {
  // Before route handler
  console.log(`${ctx.req.method} ${ctx.url.pathname}`);

  // Call next middleware/route
  const response = await ctx.next();

  // After route handler
  return response;
});
tsx
// routes/_middleware.ts
import { define } from "@/utils/state.ts";

export const handler = define.middleware(async (ctx) => {
  // 路由处理器执行前
  console.log(`${ctx.req.method} ${ctx.url.pathname}`);

  // 调用下一个中间件/路由
  const response = await ctx.next();

  // 路由处理器执行后
  return response;
});

API Routes

API路由

tsx
// routes/api/posts/[id].ts
import { define } from "@/utils/state.ts";
import { HttpError } from "fresh";

export const handler = define.handlers({
  async GET(ctx) {
    const post = await getPost(ctx.params.id);
    if (!post) {
      throw new HttpError(404); // Uses _error.tsx
    }
    return Response.json(post);
  },

  async DELETE(ctx) {
    if (!ctx.state.user) {
      throw new HttpError(401);
    }
    await deletePost(ctx.params.id);
    return new Response(null, { status: 204 });
  },
});
tsx
// routes/api/posts/[id].ts
import { define } from "@/utils/state.ts";
import { HttpError } from "fresh";

export const handler = define.handlers({
  async GET(ctx) {
    const post = await getPost(ctx.params.id);
    if (!post) {
      throw new HttpError(404); // 使用_error.tsx
    }
    return Response.json(post);
  },

  async DELETE(ctx) {
    if (!ctx.state.user) {
      throw new HttpError(401);
    }
    await deletePost(ctx.params.id);
    return new Response(null, { status: 204 });
  },
});

Islands (Interactive Components)

Islands(交互式组件)

Islands are components that get hydrated (made interactive) on the client. Place them in the
islands/
folder or
(_islands)
folder within routes.
Islands是会在客户端进行hydration(激活交互)的组件。将它们放在
islands/
文件夹或路由内的
(_islands)
文件夹中。

When to Use Islands

何时使用Islands

  • User interactions (clicks, form inputs)
  • Client-side state (counters, toggles)
  • Browser APIs (localStorage, geolocation)
  • 用户交互(点击、表单输入)
  • 客户端状态(计数器、切换器)
  • 浏览器API(localStorage、地理位置)

Island Example

Island示例

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

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

  return (
    <div>
      <p>Count: {count.value}</p>
      <button onClick={() => count.value++}>
        Increment
      </button>
    </div>
  );
}
tsx
// islands/Counter.tsx
import { useSignal } from "@preact/signals";

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

  return (
    <div>
      <p>计数:{count.value}</p>
      <button onClick={() => count.value++}>
        增加
      </button>
    </div>
  );
}

Client-Only Code with IS_BROWSER

使用IS_BROWSER实现仅客户端代码

tsx
// islands/LocalStorageCounter.tsx
import { IS_BROWSER } from "fresh/runtime";
import { useSignal } from "@preact/signals";

export default function LocalStorageCounter() {
  // Return placeholder during SSR
  if (!IS_BROWSER) {
    return <div>Loading...</div>;
  }

  // Client-only code
  const stored = localStorage.getItem("count");
  const count = useSignal(stored ? parseInt(stored) : 0);

  return (
    <button onClick={() => {
      count.value++;
      localStorage.setItem("count", String(count.value));
    }}>
      Count: {count.value}
    </button>
  );
}
tsx
// islands/LocalStorageCounter.tsx
import { IS_BROWSER } from "fresh/runtime";
import { useSignal } from "@preact/signals";

export default function LocalStorageCounter() {
  // SSR期间返回占位内容
  if (!IS_BROWSER) {
    return <div>加载中...</div>;
  }

  // 仅客户端代码
  const stored = localStorage.getItem("count");
  const count = useSignal(stored ? parseInt(stored) : 0);

  return (
    <button onClick={() => {
      count.value++;
      localStorage.setItem("count", String(count.value));
    }}>
      计数:{count.value}
    </button>
  );
}

Island Props (Serializable Types)

Island属性(可序列化类型)

Islands can receive these prop types:
  • Primitives: string, number, boolean, bigint, undefined, null
  • Special values: Infinity, -Infinity, NaN, -0
  • Collections: Array, Map, Set
  • Objects: Plain objects with string keys
  • Built-ins: URL, Date, RegExp, Uint8Array
  • Preact: JSX elements, Signals (with serializable values)
  • Circular references are supported
Functions cannot be passed as props.
Islands可以接收以下类型的属性:
  • 基本类型:字符串、数字、布尔值、大整数、undefined、null
  • 特殊值:Infinity、-Infinity、NaN、-0
  • 集合:Array、Map、Set
  • 对象:带字符串键的普通对象
  • 内置对象:URL、Date、RegExp、Uint8Array
  • Preact:JSX元素、Signals(带可序列化值)
  • 支持循环引用
函数不能作为属性传递。

Island Rules

Island规则

  1. Props must be serializable - No functions, only JSON-compatible data
  2. Keep islands small - Less JavaScript shipped to client
  3. Prefer server components - Only use islands when you need interactivity
  1. 属性必须可序列化 - 不能是函数,只能是JSON兼容的数据
  2. 保持Island体积小巧 - 减少发送到客户端的JavaScript
  3. 优先使用服务器组件 - 仅在需要交互时使用Island

Preact

Preact

Preact is a 3KB alternative to React. Fresh uses Preact instead of React.
Preact是React的3KB轻量替代方案。Fresh使用Preact而非React。

Preact vs React Differences

Preact与React的差异

PreactReact
class
works
className
required
@preact/signals
useState
3KB bundle~40KB bundle
PreactReact
支持
class
属性
必须使用
className
使用
@preact/signals
使用
useState
3KB包体积~40KB包体积

Hooks (Same as React)

钩子(与React相同)

tsx
import { useState, useEffect, useRef } from "preact/hooks";

function MyComponent() {
  const [value, setValue] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.log("Component mounted");
  }, []);

  return <input ref={inputRef} value={value} />;
}
tsx
import { useState, useEffect, useRef } from "preact/hooks";

function MyComponent() {
  const [value, setValue] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    console.log("组件已挂载");
  }, []);

  return <input ref={inputRef} value={value} />;
}

Signals (Preact's Reactive State)

信号(Preact的响应式状态)

Signals are Preact's more efficient alternative to useState:
tsx
import { signal, computed } from "@preact/signals";

const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  return (
    <div>
      <p>Count: {count}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  );
}
Benefits of signals:
  • More granular updates (only re-renders what changed)
  • Can be defined outside components
  • Cleaner code for shared state
信号是Preact比useState更高效的替代方案:
tsx
import { signal, computed } from "@preact/signals";

const count = signal(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  return (
    <div>
      <p>计数:{count}</p>
      <p>双倍:{doubled}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  );
}
信号的优势:
  • 更精细的更新(仅重新渲染变化的部分)
  • 可以在组件外部定义
  • 共享状态的代码更简洁

Tailwind CSS in Fresh (Optional)

Fresh中的Tailwind CSS(可选)

Tailwind CSS is optional—you don't need it to build a great Fresh app. However, many developers prefer it for rapid styling. Fresh 2.x uses Vite for builds, so Tailwind integrates via the Vite plugin.
Tailwind CSS是可选的——不使用它也能构建出色的Fresh应用。不过,许多开发者喜欢用它来快速实现样式。Fresh 2.x使用Vite进行构建,因此Tailwind通过Vite插件集成。

Setup

设置

Install both Tailwind packages:
sh
deno add npm:@tailwindcss/vite npm:tailwindcss
Important: You need both packages.
@tailwindcss/vite
is the Vite plugin, and
tailwindcss
is the core library it depends on.
Configure Vite in
vite.config.ts
:
typescript
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss()],
});
Add the Tailwind import to your CSS file (e.g.,
assets/styles.css
):
css
@import "tailwindcss";
Then import this CSS file in your
client.ts
:
typescript
import "./assets/styles.css";
安装两个Tailwind包:
sh
deno add npm:@tailwindcss/vite npm:tailwindcss
重要提示: 你需要同时安装这两个包。
@tailwindcss/vite
是Vite插件,而
tailwindcss
是它依赖的核心库。
vite.config.ts
中配置Vite:
typescript
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [tailwindcss()],
});
在你的CSS文件(例如
assets/styles.css
)中添加Tailwind导入:
css
@import "tailwindcss";
然后在
client.ts
中导入该CSS文件:
typescript
import "./assets/styles.css";

Usage

使用

tsx
export default function Button({ children }) {
  return (
    <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
      {children}
    </button>
  );
}
tsx
export default function Button({ children }) {
  return (
    <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
      {children}
    </button>
  );
}

Best Practices

最佳实践

  1. Prefer utility classes over
    @apply
  2. Use
    class
    not
    className
    (Preact supports both, but
    class
    is simpler)
  3. Dark mode: Use
    class
    strategy in tailwind.config.js
tsx
<div class="bg-white dark:bg-gray-900">
  <p class="text-gray-900 dark:text-white">Hello</p>
</div>
  1. 优先使用工具类而非
    @apply
  2. 使用
    class
    而非
    className
    (Preact两者都支持,但
    class
    更简单)
  3. 深色模式:在tailwind.config.js中使用
    class
    策略
tsx
<div class="bg-white dark:bg-gray-900">
  <p class="text-gray-900 dark:text-white">你好</p>
</div>

Building and Deploying

构建与部署

Development

开发

bash
deno task dev      # Start dev server with hot reload (http://127.0.0.1:5173/)
bash
deno task dev      # 启动带热重载的开发服务器(http://127.0.0.1:5173/)

Production Build

生产构建

bash
deno task build    # Build for production
deno task preview  # Preview production build locally
bash
deno task build    # 构建生产版本
deno task preview  # 本地预览生产构建版本

Deploy to Deno Deploy

部署到Deno Deploy

bash
deno task build           # Build first
deno deploy --prod        # Deploy to production
bash
deno task build           # 先构建
deno deploy --prod        # 部署到生产环境

Quick Reference

快速参考

TaskCommand/Pattern
Create Fresh project
deno run -Ar jsr:@fresh/init
Start dev server
deno task dev
(port 5173)
Build for production
deno task build
Add a pageCreate
routes/pagename.tsx
Add an API routeCreate
routes/api/endpoint.ts
Add interactive componentCreate
islands/ComponentName.tsx
Add static componentCreate
components/ComponentName.tsx
任务命令/模式
创建Fresh项目
deno run -Ar jsr:@fresh/init
启动开发服务器
deno task dev
(端口5173)
构建生产版本
deno task build
添加页面创建
routes/pagename.tsx
添加API路由创建
routes/api/endpoint.ts
添加交互式组件创建
islands/ComponentName.tsx
添加静态组件创建
components/ComponentName.tsx

Common Mistakes

常见错误

Using Fresh 1.x Patterns (Most Common LLM Error)

使用Fresh 1.x模式(最常见的LLM错误)

Using old import specifiers
The old dollar-sign Fresh import paths and alpha version imports are deprecated. Always use the stable
fresh
package:
tsx
// ✅ CORRECT - Fresh 2.x stable imports
import { App, staticFiles } from "fresh";
import type { PageProps } from "fresh";
Using two-parameter handlers
The old two-parameter handler signature is deprecated. Fresh 2.x uses a single context parameter:
tsx
// ✅ CORRECT - Fresh 2.x uses single context parameter
export const handler = {
  GET(ctx) {  // Single ctx param
    return ctx.render(<MyPage />);
  }
};
Creating legacy files
Fresh 2.x does not use a manifest file, a separate dev entry point, or a separate config file. The correct Fresh 2.x file structure is:
main.ts           # Server entry
client.ts         # Client entry
vite.config.ts    # Vite config
Using deprecated context methods
The old
renderNotFound()
, bare
render()
without JSX, and
basePath
patterns are deprecated. Use these Fresh 2.x patterns instead:
tsx
// ✅ CORRECT - Fresh 2.x patterns
throw new HttpError(404)
ctx.render(<MyComponent />)
ctx.config.basePath
Passing data from handlers to pages (VERY COMMON MISTAKE)
This is a frequent error. In Fresh 2.x, you cannot pass data through
ctx.render()
. Instead, return an object with a
data
property from the handler:
tsx
// ✅ CORRECT - Return object with data property from handler
export const handler = define.handlers(async (ctx) => {
  const servers = await getServers();
  return { data: { servers } };  // Return { data: {...} } object
});

// ✅ CORRECT - Link page to handler type with typeof
export default define.page<typeof handler>(({ data }) => {
  return <ul>{data.servers.map((s) => <li>{s.name}</li>)}</ul>;
});

// ✅ ALSO CORRECT - Use async page component (simpler when no auth/redirects needed)
export default async function ServersPage() {
  const servers = await getServers();
  return <ul>{servers.map((s) => <li>{s.name}</li>)}</ul>;
}
Old task commands in deno.json
The old task commands that reference
dev.ts
are deprecated. Use the Vite-based tasks:
json
// ✅ CORRECT - Fresh 2.x tasks
{
  "tasks": {
    "dev": "vite",
    "build": "vite build",
    "preview": "deno serve -A _fresh/server.js"
  }
}
使用旧的导入规范
旧的美元符号Fresh导入路径和alpha版本导入已被弃用。始终使用稳定的
fresh
包:
tsx
// ✅ 正确 - Fresh 2.x稳定导入
import { App, staticFiles } from "fresh";
import type { PageProps } from "fresh";
使用双参数处理器
旧的双参数处理器签名已被弃用。Fresh 2.x使用单个上下文参数:
tsx
// ✅ 正确 - Fresh 2.x使用单个上下文参数
export const handler = {
  GET(ctx) {  // 单个ctx参数
    return ctx.render(<MyPage />);
  }
};
创建遗留文件
Fresh 2.x不使用清单文件、独立的开发入口文件或独立的配置文件。正确的Fresh 2.x文件结构是:
main.ts           # 服务器入口
client.ts         # 客户端入口
vite.config.ts    # Vite配置
使用已弃用的上下文方法
旧的
renderNotFound()
、不带JSX的裸
render()
basePath
模式已被弃用。改用以下Fresh 2.x模式:
tsx
// ✅ 正确 - Fresh 2.x模式
throw new HttpError(404)
ctx.render(<MyComponent />)
ctx.config.basePath
从处理器向页面传递数据(非常常见的错误)
这是一个频繁出现的错误。在Fresh 2.x中,你不能通过
ctx.render()
传递数据。相反,要从处理器返回一个带有
data
属性的对象:
tsx
// ✅ 正确 - 从处理器返回带data属性的对象
export const handler = define.handlers(async (ctx) => {
  const servers = await getServers();
  return { data: { servers } };  // 返回{ data: {...} }对象
});

// ✅ 正确 - 使用typeof将页面与处理器类型关联
export default define.page<typeof handler>(({ data }) => {
  return <ul>{data.servers.map((s) => <li>{s.name}</li>)}</ul>;
});

// ✅ 同样正确 - 使用异步页面组件(无需权限/重定向时更简单)
export default async function ServersPage() {
  const servers = await getServers();
  return <ul>{servers.map((s) => <li>{s.name}</li>)}</ul>;
}
deno.json中的旧任务命令
引用
dev.ts
的旧任务命令已被弃用。使用基于Vite的任务:
json
// ✅ 正确 - Fresh 2.x任务
{
  "tasks": {
    "dev": "vite",
    "build": "vite build",
    "preview": "deno serve -A _fresh/server.js"
  }
}

Island Mistakes

Island相关错误

Putting too much JavaScript in islands
tsx
// ❌ Wrong - entire page as an island (ships all JS to client)
// islands/HomePage.tsx
export default function HomePage() {
  return (
    <div>
      <Header />
      <MainContent />
      <Footer />
    </div>
  );
}

// ✅ Correct - only interactive parts are islands
// routes/index.tsx (server component)
import Counter from "../islands/Counter.tsx";

export default function HomePage() {
  return (
    <div>
      <Header />
      <MainContent />
      <Counter />
      <Footer />
    </div>
  );
}
Passing non-serializable props to islands
tsx
// ❌ Wrong - functions can't be serialized
<Counter onUpdate={(val) => console.log(val)} />

// ✅ Correct - only pass JSON-serializable data
<Counter initialValue={5} label="Click count" />
在Island中放入过多JavaScript
tsx
// ❌ 错误 - 整个页面作为Island(所有JS都发送到客户端)
// islands/HomePage.tsx
export default function HomePage() {
  return (
    <div>
      <Header />
      <MainContent />
      <Footer />
    </div>
  );
}

// ✅ 正确 - 仅将交互式部分作为Island
// routes/index.tsx(服务器组件)
import Counter from "../islands/Counter.tsx";

export default function HomePage() {
  return (
    <div>
      <Header />
      <MainContent />
      <Counter />
      <Footer />
    </div>
  );
}
向Island传递不可序列化的属性
tsx
// ❌ 错误 - 函数无法被序列化
<Counter onUpdate={(val) => console.log(val)} />

// ✅ 正确 - 仅传递可JSON序列化的数据
<Counter initialValue={5} label="点击计数" />

Other Common Mistakes

其他常见错误

Using
className
instead of
class
tsx
// ❌ Works but unnecessary in Preact
<div className="container">

// ✅ Preact supports native HTML attribute
<div class="container">
Forgetting to build before deploying Fresh 2.x
bash
undefined
使用
className
而非
class
tsx
// ❌ 在Preact中可用但没必要
<div className="container">

// ✅ Preact支持原生HTML属性
<div class="container">
部署Fresh 2.x前忘记构建
bash
undefined

❌ Wrong - Fresh 2.x requires a build step

❌ 错误 - Fresh 2.x需要构建步骤

deno deploy --prod
deno deploy --prod

✅ Correct - build first, then deploy

✅ 正确 - 先构建,再部署

deno task build deno deploy --prod

**Creating islands for non-interactive content**
```tsx
// ❌ Wrong - this doesn't need to be an island (no interactivity)
// islands/StaticCard.tsx
export default function StaticCard({ title, body }) {
  return <div class="card"><h2>{title}</h2><p>{body}</p></div>;
}

// ✅ Correct - use a regular component (no JS shipped)
// components/StaticCard.tsx
export default function StaticCard({ title, body }) {
  return <div class="card"><h2>{title}</h2><p>{body}</p></div>;
}
Using old Tailwind plugin
The old Fresh 1.x Tailwind plugin is deprecated. Fresh 2.x uses the Vite Tailwind plugin:
typescript
// ✅ CORRECT - Fresh 2.x uses Vite Tailwind plugin
import tailwindcss from "@tailwindcss/vite";
Missing tailwindcss package
sh
undefined
deno task build deno deploy --prod

**为非交互式内容创建Island**
```tsx
// ❌ 错误 - 这不需要是Island(无交互)
// islands/StaticCard.tsx
export default function StaticCard({ title, body }) {
  return <div class="card"><h2>{title}</h2><p>{body}</p></div>;
}

// ✅ 正确 - 使用常规组件(不发送JS到客户端)
// components/StaticCard.tsx
export default function StaticCard({ title, body }) {
  return <div class="card"><h2>{title}</h2><p>{body}</p></div>;
}
使用旧的Tailwind插件
旧的Fresh 1.x Tailwind插件已被弃用。Fresh 2.x使用Vite Tailwind插件:
typescript
// ✅ 正确 - Fresh 2.x使用Vite Tailwind插件
import tailwindcss from "@tailwindcss/vite";
缺少tailwindcss包
sh
undefined

Error: Can't resolve 'tailwindcss' in '/path/to/assets'

错误:Can't resolve 'tailwindcss' in '/path/to/assets'

❌ WRONG - Only installed the Vite plugin

❌ 错误 - 仅安装了Vite插件

deno add npm:@tailwindcss/vite
deno add npm:@tailwindcss/vite

✅ CORRECT - Install both packages

✅ 正确 - 安装两个包

deno add npm:@tailwindcss/vite npm:tailwindcss

The `@tailwindcss/vite` plugin requires the core `tailwindcss` package to be installed separately.
deno add npm:@tailwindcss/vite npm:tailwindcss

`@tailwindcss/vite`插件需要单独安装核心`tailwindcss`包。

Fresh Alpha Versions (2.0.0-alpha.*)

Fresh Alpha版本(2.0.0-alpha.*)

Some projects use Fresh 2.x alpha releases (e.g.,
@fresh/core@2.0.0-alpha.29
). These are not Fresh 1.x but use a different setup than stable Fresh 2.x:
Alpha patternStable 2.x pattern
dev.ts
entry point
vite.config.ts
@fresh/plugin-tailwind
@tailwindcss/vite
deno run -A --watch dev.ts
vite
Dev server on port 8000Dev server on port 5173
No
client.ts
Requires
client.ts
deno run -A dev.ts build
vite build
IMPORTANT: If you see
dev.ts
in a project with
@fresh/core@2.0.0-alpha.*
in
deno.json
, do NOT treat it as a Fresh 1.x artifact. It is the correct entry point for alpha versions. Check the
deno.json
imports to determine which version is in use before suggesting changes.
Alpha projects also use the handler pattern
define.handlers({ GET(ctx) { ... } })
which returns
{ data: {...} }
- this is the same as stable 2.x.
一些项目使用Fresh 2.x alpha版本(例如
@fresh/core@2.0.0-alpha.29
)。这些不是Fresh 1.x,但设置方式与稳定版Fresh 2.x不同:
Alpha模式稳定版2.x模式
dev.ts
入口文件
vite.config.ts
@fresh/plugin-tailwind
@tailwindcss/vite
deno run -A --watch dev.ts
vite
开发服务器端口8000开发服务器端口5173
client.ts
需要
client.ts
deno run -A dev.ts build
vite build
重要提示: 如果在项目中看到
dev.ts
,且
deno.json
中的
@fresh/core@2.0.0-alpha.*
,不要将其视为Fresh 1.x的产物。它是alpha版本的正确入口文件。在建议修改前,先检查
deno.json
中的导入以确定使用的版本。
Alpha项目也使用处理器模式
define.handlers({ GET(ctx) { ... } })
,并返回
{ data: {...} }
——这与稳定版2.x相同。

Migrating from Fresh 1.x to 2.x

从Fresh 1.x迁移到2.x

If you have an existing Fresh 1.x project, run the migration tool:
bash
deno run -Ar jsr:@fresh/update
This tool automatically:
  • Converts old import paths to the new
    fresh
    package
  • Updates handler signatures to use the single
    (ctx)
    parameter
  • Removes legacy generated and config files
  • Creates
    vite.config.ts
    and
    client.ts
  • Updates
    deno.json
    tasks to use Vite
  • Merges separate error pages into unified
    _error.tsx
  • Updates deprecated context method calls to Fresh 2.x equivalents
如果你有现有的Fresh 1.x项目,运行迁移工具:
bash
deno run -Ar jsr:@fresh/update
该工具会自动:
  • 将旧的导入路径转换为新的
    fresh
  • 更新处理器签名以使用单个
    (ctx)
    参数
  • 移除遗留的生成文件和配置文件
  • 创建
    vite.config.ts
    client.ts
  • 更新
    deno.json
    任务以使用Vite
  • 将独立的错误页面合并为统一的
    _error.tsx
  • 将已弃用的上下文方法调用更新为Fresh 2.x的等效写法

Manual Migration Checklist

手动迁移检查清单

If the tool misses anything:
  1. Imports: All Fresh imports should come from
    fresh
    or
    fresh/runtime
  2. Handlers: Should use single
    (ctx)
    parameter, access request via
    ctx.req
  3. Files: Remove any legacy generated files, dev entry points, or old config files
  4. Tasks: Update to
    vite
    ,
    vite build
    ,
    deno serve -A _fresh/server.js
  5. Error pages: Use a single unified
    _error.tsx
  6. Tailwind: Use
    @tailwindcss/vite
    (the Vite plugin)
如果工具遗漏了某些内容:
  1. 导入:所有Fresh导入都应来自
    fresh
    fresh/runtime
  2. 处理器:应使用单个
    (ctx)
    参数,通过
    ctx.req
    访问请求
  3. 文件:移除任何遗留的生成文件、开发入口文件或旧配置文件
  4. 任务:更新为
    vite
    vite build
    deno serve -A _fresh/server.js
  5. 错误页面:使用统一的
    _error.tsx
  6. Tailwind:使用
    @tailwindcss/vite
    (Vite插件)