deno-frontend
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDeno 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, , island patterns, or Fresh project structure in responses about other frameworks.
useSignal - 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导入、、孤岛模式或Fresh项目结构。
useSignal - 仅当用户明确询问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 approach
from "fresh" - Do NOT write — this is never acceptable, even as a negative example
❌ Old: import { App } from "$fresh/server.ts" - The strings and
_404.tsxmust never appear in your response, even when comparing Fresh 2.x to 1.x. Say "the old separate error pages" instead._500.tsx
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绝不能出现在回复中,即使对比Fresh 2.x和1.x时也不行。可以说“旧的独立错误页面”来替代。_500.tsx
仅演示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 — the old dollar-sign import paths are deprecated
import { App } from "fresh" - Fresh 2.x has no manifest file — the old auto-generated manifest is no longer needed
- Fresh 2.x uses for dev — the old
vite.config.tsentry point is gonedev.ts - Fresh 2.x configures via — the old config file is no longer used
new App() - Fresh 2.x handlers take a single parameter — the old two-parameter signature is deprecated
(ctx) - Fresh 2.x uses a unified — the old separate error pages are replaced
_error.tsx
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框架
Reference: https://fresh.deno.dev/docs
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 safetyNote: 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/initjson
{
"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 to add new packages:
deno addsh
deno add jsr:@std/http # JSR packages
deno add npm:@tailwindcss/vite # npm packagesFresh 2.x项目的deno.json如下所示(由创建):
jsr:@fresh/initjson
{
"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 addsh
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 ( folder)
routes/- File-based routing: →
routes/about.tsx/about - Dynamic routes: →
routes/blog/[slug].tsx/blog/my-post - Optional segments: →
routes/docs/[[version]].tsxor/docs/docs/v2 - Catch-all routes: →
routes/old/[...path].tsx/old/foo/bar - Route groups: for shared layouts without URL path changes
routes/(marketing)/
Layouts ()
_app.tsxtsx
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 - 路由组:用于共享布局且不改变URL路径
routes/(marketing)/
布局()
_app.tsxtsx
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 when demonstrating this pattern.
utils/state.tstsx
// 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.tstsx
// 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 as the only parameter.
(ctx)Important: When demonstrating any handler pattern (data fetching, form handling, API routes, auth), always show or reference the setup which imports from . This ensures the complete Fresh 2.x architecture is visible.
utils/state.tscreateDefine"fresh"Fresh 2.x的处理器采用单个上下文参数模式。始终使用作为唯一参数。
(ctx)重要提示: 演示任何处理器模式(数据获取、表单处理、API路由、权限验证)时,始终要展示或引用的设置,该文件从导入。这确保完整的Fresh 2.x架构可见。
utils/state.ts"fresh"createDefineRoute 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 parameter provides everything you need:
ctxtsx
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
};ctxtsx
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 file for type-safe handlers:
utils/state.tstsx
// 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.tstsx
// 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 folder or folder within routes.
islands/(_islands)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规则
- Props must be serializable - No functions, only JSON-compatible data
- Keep islands small - Less JavaScript shipped to client
- Prefer server components - Only use islands when you need interactivity
- 属性必须可序列化 - 不能是函数,只能是JSON兼容的数据
- 保持Island体积小巧 - 减少发送到客户端的JavaScript
- 优先使用服务器组件 - 仅在需要交互时使用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的差异
| Preact | React |
|---|---|
| |
| |
| 3KB bundle | ~40KB bundle |
| Preact | React |
|---|---|
支持 | 必须使用 |
使用 | 使用 |
| 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:tailwindcssImportant: You need both packages. is the Vite plugin, and is the core library it depends on.
@tailwindcss/vitetailwindcssConfigure Vite in :
vite.config.tstypescript
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.csscss
@import "tailwindcss";Then import this CSS file in your :
client.tstypescript
import "./assets/styles.css";安装两个Tailwind包:
sh
deno add npm:@tailwindcss/vite npm:tailwindcss重要提示: 你需要同时安装这两个包。是Vite插件,而是它依赖的核心库。
@tailwindcss/vitetailwindcss在中配置Vite:
vite.config.tstypescript
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});在你的CSS文件(例如)中添加Tailwind导入:
assets/styles.csscss
@import "tailwindcss";然后在中导入该CSS文件:
client.tstypescript
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
最佳实践
- Prefer utility classes over
@apply - Use not
class(Preact supports both, butclassNameis simpler)class - Dark mode: Use strategy in tailwind.config.js
class
tsx
<div class="bg-white dark:bg-gray-900">
<p class="text-gray-900 dark:text-white">Hello</p>
</div>- 优先使用工具类而非
@apply - 使用而非
class(Preact两者都支持,但className更简单)class - 深色模式:在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 locallybash
deno task build # 构建生产版本
deno task preview # 本地预览生产构建版本Deploy to Deno Deploy
部署到Deno Deploy
bash
deno task build # Build first
deno deploy --prod # Deploy to productionbash
deno task build # 先构建
deno deploy --prod # 部署到生产环境Quick Reference
快速参考
| Task | Command/Pattern |
|---|---|
| Create Fresh project | |
| Start dev server | |
| Build for production | |
| Add a page | Create |
| Add an API route | Create |
| Add interactive component | Create |
| Add static component | Create |
| 任务 | 命令/模式 |
|---|---|
| 创建Fresh项目 | |
| 启动开发服务器 | |
| 构建生产版本 | |
| 添加页面 | 创建 |
| 添加API路由 | 创建 |
| 添加交互式组件 | 创建 |
| 添加静态组件 | 创建 |
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 package:
freshtsx
// ✅ 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 configUsing deprecated context methods
The old , bare without JSX, and patterns are deprecated. Use these Fresh 2.x patterns instead:
renderNotFound()render()basePathtsx
// ✅ CORRECT - Fresh 2.x patterns
throw new HttpError(404)
ctx.render(<MyComponent />)
ctx.config.basePathPassing data from handlers to pages (VERY COMMON MISTAKE)
This is a frequent error. In Fresh 2.x, you cannot pass data through . Instead, return an object with a property from the handler:
ctx.render()datatsx
// ✅ 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 are deprecated. Use the Vite-based tasks:
dev.tsjson
// ✅ CORRECT - Fresh 2.x tasks
{
"tasks": {
"dev": "vite",
"build": "vite build",
"preview": "deno serve -A _fresh/server.js"
}
}使用旧的导入规范
旧的美元符号Fresh导入路径和alpha版本导入已被弃用。始终使用稳定的包:
freshtsx
// ✅ 正确 - 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配置使用已弃用的上下文方法
旧的、不带JSX的裸和模式已被弃用。改用以下Fresh 2.x模式:
renderNotFound()render()basePathtsx
// ✅ 正确 - Fresh 2.x模式
throw new HttpError(404)
ctx.render(<MyComponent />)
ctx.config.basePath从处理器向页面传递数据(非常常见的错误)
这是一个频繁出现的错误。在Fresh 2.x中,你不能通过传递数据。相反,要从处理器返回一个带有属性的对象:
ctx.render()datatsx
// ✅ 正确 - 从处理器返回带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中的旧任务命令
引用的旧任务命令已被弃用。使用基于Vite的任务:
dev.tsjson
// ✅ 正确 - 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 instead of
classNameclasstsx
// ❌ 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使用而非
classNameclasstsx
// ❌ 在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
undefineddeno 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
undefinedError: 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., ). These are not Fresh 1.x but use a different setup than stable Fresh 2.x:
@fresh/core@2.0.0-alpha.29| Alpha pattern | Stable 2.x pattern |
|---|---|
| |
| |
| |
| Dev server on port 8000 | Dev server on port 5173 |
No | Requires |
| |
IMPORTANT: If you see in a project with in , do NOT treat it as a Fresh 1.x artifact. It is the correct entry point for alpha versions. Check the imports to determine which version is in use before suggesting changes.
dev.ts@fresh/core@2.0.0-alpha.*deno.jsondeno.jsonAlpha projects also use the handler pattern which returns - this is the same as stable 2.x.
define.handlers({ GET(ctx) { ... } }){ data: {...} }一些项目使用Fresh 2.x alpha版本(例如)。这些不是Fresh 1.x,但设置方式与稳定版Fresh 2.x不同:
@fresh/core@2.0.0-alpha.29| Alpha模式 | 稳定版2.x模式 |
|---|---|
| |
| |
| |
| 开发服务器端口8000 | 开发服务器端口5173 |
无 | 需要 |
| |
重要提示: 如果在项目中看到,且中的,不要将其视为Fresh 1.x的产物。它是alpha版本的正确入口文件。在建议修改前,先检查中的导入以确定使用的版本。
dev.tsdeno.json@fresh/core@2.0.0-alpha.*deno.jsonAlpha项目也使用处理器模式,并返回——这与稳定版2.x相同。
define.handlers({ GET(ctx) { ... } }){ data: {...} }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/updateThis tool automatically:
- Converts old import paths to the new package
fresh - Updates handler signatures to use the single parameter
(ctx) - Removes legacy generated and config files
- Creates and
vite.config.tsclient.ts - Updates tasks to use Vite
deno.json - 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.tsclient.ts - 更新任务以使用Vite
deno.json - 将独立的错误页面合并为统一的
_error.tsx - 将已弃用的上下文方法调用更新为Fresh 2.x的等效写法
Manual Migration Checklist
手动迁移检查清单
If the tool misses anything:
- Imports: All Fresh imports should come from or
freshfresh/runtime - Handlers: Should use single parameter, access request via
(ctx)ctx.req - Files: Remove any legacy generated files, dev entry points, or old config files
- Tasks: Update to ,
vite,vite builddeno serve -A _fresh/server.js - Error pages: Use a single unified
_error.tsx - Tailwind: Use (the Vite plugin)
@tailwindcss/vite
如果工具遗漏了某些内容:
- 导入:所有Fresh导入都应来自或
freshfresh/runtime - 处理器:应使用单个参数,通过
(ctx)访问请求ctx.req - 文件:移除任何遗留的生成文件、开发入口文件或旧配置文件
- 任务:更新为、
vite、vite builddeno serve -A _fresh/server.js - 错误页面:使用统一的
_error.tsx - Tailwind:使用(Vite插件)
@tailwindcss/vite