关于 deno-frontend
deno-frontend 是使用 Deno 和 Fresh 2.x 构建前端的技能,涵盖 Preact 与 Tailwind CSS。它介绍服务端渲染页面、路由及按需激活的交互式 islands,并遵循当前 Fresh 模式。
主要功能
- 01Fresh 2.x 应用与路由模式
- 02服务端渲染页面和数据获取
- 03使用 Preact 构建交互式 islands
- 04Tailwind CSS 集成指南
- 05Fresh 2.x 迁移与语法规则
适用场景
- 01启动 Fresh 2.x Web 应用
- 02构建服务端渲染路由和 API 端点
- 03通过 islands 添加交互界面
- 04使用 Tailwind CSS 为 Deno 前端设定样式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ midudev/ autoskills --skill deno-frontend安装后在 Claude Code 中输入/deno-frontend确认已加载。
详细介绍
Deno 前端开发
概述
本技能涵盖使用 Fresh 2.x(Deno 的 Web 框架)、Preact(轻量级 React 替代方案)和 Tailwind CSS 进行 Deno 前端开发。Fresh 使用「岛屿架构」:页面在服务器端渲染,只有可交互部分会将 JavaScript 发送到浏览器。
何时使用此技能
- 创建新的 Fresh Web 应用
- 构建可交互的 UI 组件(岛屿)
- 添加服务器端渲染的页面和路由
- 集成 Tailwind CSS 进行样式设计
- 选择使用岛屿(客户端)还是组件(仅服务器端)
- 使用 Preact hooks 和 signals
在 Deno 中构建 Web 应用时,请遵循这些实践。
适用范围
此技能仅适用于 Fresh/Deno 前端相关问题。请遵循以下规则:
- 如果用户询问 Next.js、React Native、Vue、Svelte、Angular 或任何其他框架,请直接使用相应框架作答。不要推荐 Fresh 替代方案。
- 回答其他框架相关问题时,不要加入 Fresh 导入、
useSignal、岛屿模式或 Fresh 项目结构。 - 只有在用户明确询问 Fresh,或正在使用 Fresh/Deno 项目时,才推荐 Fresh。
重要:绝不展示已弃用语法
帮助用户从 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 模式。
重要:Fresh 2.x 与 1.x 的区别
始终使用 Fresh 2.x 模式。 Fresh 1.x 已弃用。主要区别如下:
- Fresh 2.x 使用
import { App } from "fresh"——旧的美元符号导入路径已弃用 - Fresh 2.x 不再使用 manifest 文件——不再需要旧的自动生成 manifest
- 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 稳定导入:
// ✅ CORRECT - Fresh 2.x stable
import { App, staticFiles } from "fresh";
import { define } from "./utils/state.ts"; // Project-local define helpers
Fresh 框架
参考:https://fresh.deno.dev/docs
Fresh 是 Deno 的 Web 框架。它采用岛屿架构:页面在服务器端渲染,只有可交互部分(「岛屿」)才会在客户端运行 JavaScript。
创建 Fresh 项目
deno run -Ar jsr:@fresh/init
cd my-project
deno task dev # Runs at http://127.0.0.1:5173/
项目结构(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
**注意:**Fresh 2.x 不使用 manifest 文件、独立的开发入口点或独立的配置文件。
main.ts(Fresh 2.x 入口点)
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
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [
fresh(),
tailwindcss(),
],
});
deno.json 配置
Fresh 2.x 项目的 deno.json 如下所示(由 jsr:@fresh/init 创建):
{
"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 添加新软件包:
deno add jsr:@std/http # JSR packages
deno add npm:@tailwindcss/vite # npm packages
导入参考(Fresh 2.x)
// 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";
核心概念
路由(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)
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>
);
}
异步服务器组件
export default async function Page() {
const data = await fetchData(); // Runs on server only
return <div>{data.title}</div>;
}
数据获取模式
Fresh 2.x 提供两种在服务端获取数据的方式。处理器模式是推荐的默认方式,因为它展示了完整的 Fresh 2.x 架构,并提供了最大的灵活性。
方式 A:带数据对象的处理器(推荐)
将此方式作为数据获取的默认方式。它采用完整的 Fresh 2.x 处理器模式,并通过类型化的数据传递数据。演示此模式时,务必展示完整的设置,包括 utils/state.ts。
// 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>();
// 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>
);
});
此方式还支持在渲染前进行身份验证检查、重定向和其他逻辑。
方式 B:异步服务端组件(简写)
对于只需获取并显示数据、无需身份验证或重定向的简单场景:
// 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>
);
}
选择指南
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
处理器和 Define 辅助工具(Fresh 2.x)
Fresh 2.x 的处理器采用单一上下文参数模式。始终只使用 (ctx) 作为参数。
重要提示: 演示任何处理器模式(数据获取、表单处理、API 路由、身份验证)时,务必展示或引用 utils/state.ts 的设置,其中从 "fresh" 导入 createDefine。这样可以呈现完整的 Fresh 2.x 架构。
路由处理器
在基于文件的路由中,始终使用 define.handlers() 来定义类型安全的路由处理器:
// routes/api/users.ts
import { define } from "@/utils/state.ts";
// Single function handles all methods
export const handler = define.handlers((ctx) => {
return new Response(`Hello from ${ctx.req.method}`);
});
// Or method-specific handlers
export const handler = define.handlers({
GET(ctx) {
return Response.json({ users: [] });
},
async POST(ctx) {
const body = await ctx.req.json();
return Response.json({ created: true }, { status: 201 });
},
});
注意:裸处理器导出(export const handler = (ctx) => {...})也可用,但会失去 TypeScript 类型安全。优先使用 define.handlers()。
上下文对象
ctx 参数提供了你所需的一切:
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
};
Define 辅助工具(类型安全)
创建 utils/state.ts 文件来定义类型安全的处理器:
// 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>();
在路由中使用:
// 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>;
});
中间件(Fresh 2.x)
// 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;
});
API 路由
// 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 });
},
});
Islands(交互式组件)
Islands 是会在客户端进行水合(变为可交互)的组件。将它们放在路由中的 islands/ 文件夹或 (_islands) 文件夹内。
何时使用 Islands
- 用户交互(点击、表单输入)
- 客户端状态(计数器、开关)
- 浏览器 API(localStorage、地理位置)
Island 示例
// 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>
);
}
使用 IS_BROWSER 编写仅客户端代码
// 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>
);
}
Island 属性(可序列化类型)
Islands 可以接收以下类型的属性:
- 基本类型:string、number、boolean、bigint、undefined、null
- 特殊值:Infinity、-Infinity、NaN、-0
- 集合:Array、Map、Set
- 对象:键为字符串的普通对象
- 内置类型:URL、Date、RegExp、Uint8Array
- Preact:JSX 元素、Signals(包含可序列化的值)
- 支持循环引用
不能将函数作为属性传递。
Island 规则
- 属性必须可序列化——不能包含函数,只能使用兼容 JSON 的数据
- 保持 Islands 精简——减少发送到客户端的 JavaScript
- 优先使用服务端组件——仅在需要交互性时使用 Islands
Preact
Preact 是 React 的 3KB 替代方案。Fresh 使用 Preact,而不是 React。
Preact 与 React 的区别
| Preact | React |
|---|---|
class 可用 | 必须使用 className |
@preact/signals | useState |
| 3KB 包体积 | ~40KB 包体积 |
Hooks(与 React 相同)
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} />;
}
Signals(Preact 的响应式状态)
Signals 是 Preact 提供的更高效的 useState 替代方案:
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>
);
}
Signals 的优点:
- 更新粒度更细(只重新渲染发生变化的部分)
- 可在组件外定义
- 共享状态的代码更简洁
Fresh 中的 Tailwind CSS(可选)
Tailwind CSS 是可选的——构建出色的 Fresh 应用并不需要它。不过,许多开发者更喜欢用它来快速设置样式。Fresh 2.x 使用 Vite 进行构建,因此 Tailwind 通过 Vite 插件集成。
设置
安装两个 Tailwind 包:
deno add npm:@tailwindcss/vite npm:tailwindcss
重要: 你需要安装这两个包。@tailwindcss/vite 是 Vite 插件,而 tailwindcss 是它依赖的核心库。
在 vite.config.ts 中配置 Vite:
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [tailwindcss()],
});
将 Tailwind 导入添加到 CSS 文件中(例如 assets/styles.css):
@import "tailwindcss";
然后在 client.ts 中导入此 CSS 文件:
import "./assets/styles.css";
用法
export default function Button({ children }) {
return (
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
{children}
</button>
);
}
最佳实践
- 优先使用工具类,而不是
@apply - 使用
class,不要使用className(Preact 两者都支持,但class更简单) - 深色模式:在 tailwind.config.js 中使用
class策略
<div class="bg-white dark:bg-gray-900">
<p class="text-gray-900 dark:text-white">Hello</p>
</div>
构建和部署
开发
deno task dev # Start dev server with hot reload (http://127.0.0.1:5173/)
生产构建
deno task build # Build for production
deno task preview # Preview production build locally
部署到 Deno Deploy
deno task build # Build first
deno deploy --prod # Deploy to production
快速参考
| 任务 | 命令/模式 |
|---|---|
| 创建 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 |
常见错误
使用 Fresh 1.x 模式(LLM 最常见的错误)
使用旧导入说明符
旧的带美元符号的 Fresh 导入路径和 alpha 版本导入已弃用。请始终使用稳定版 fresh 包:
// ✅ CORRECT - Fresh 2.x stable imports
import { App, staticFiles } from "fresh";
import type { PageProps } from "fresh";
使用双参数处理程序
旧的双参数处理程序签名已弃用。Fresh 2.x 使用单个上下文参数:
// ✅ CORRECT - Fresh 2.x uses single context parameter
export const handler = {
GET(ctx) { // Single ctx param
return ctx.render(<MyPage />);
}
};
创建旧版文件
Fresh 2.x 不使用清单文件、单独的开发入口点或单独的配置文件。正确的 Fresh 2.x 文件结构如下:
main.ts # Server entry
client.ts # Client entry
vite.config.ts # Vite config
使用已弃用的上下文方法
旧的 renderNotFound()、不带 JSX 的裸 render() 以及 basePath 模式均已弃用。请改用以下 Fresh 2.x 模式:
// ✅ CORRECT - Fresh 2.x patterns
throw new HttpError(404)
ctx.render(<MyComponent />)
ctx.config.basePath
将数据从处理程序传递到页面(非常常见的错误)
这是一个常见错误。在 Fresh 2.x 中,你不能通过 ctx.render() 传递数据。请改为从处理程序返回带有 data 属性的对象:
// ✅ 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>;
}
在 deno.json 中使用旧任务命令
引用 dev.ts 的旧任务命令已弃用。请使用基于 Vite 的任务:
// ✅ CORRECT - Fresh 2.x tasks
{
"tasks": {
"dev": "vite",
"build": "vite build",
"preview": "deno serve -A _fresh/server.js"
}
}
Island 常见错误
在 island 中放入过多 JavaScript
// ❌ 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>
);
}
向 island 传递不可序列化的属性
// ❌ Wrong - functions can't be serialized
<Counter onUpdate={(val) => console.log(val)} />
// ✅ Correct - only pass JSON-serializable data
<Counter initialValue={5} label="Click count" />
其他常见错误
使用 className 而不是 class
// ❌ Works but unnecessary in Preact
<div className="container">
// ✅ Preact supports native HTML attribute
<div class="container">
部署 Fresh 2.x 前忘记构建
# ❌ Wrong - Fresh 2.x requires a build step
deno deploy --prod
# ✅ Correct - build first, then deploy
deno task build
deno deploy --prod
为非交互式内容创建 island
// ❌ 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>;
}
使用旧 Tailwind 插件
旧的 Fresh 1.x Tailwind 插件已弃用。Fresh 2.x 使用 Vite Tailwind 插件:
// ✅ CORRECT - Fresh 2.x uses Vite Tailwind plugin
import tailwindcss from "@tailwindcss/vite";
缺少 tailwindcss 包
# Error: Can't resolve 'tailwindcss' in '/path/to/assets'
# ❌ WRONG - Only installed the Vite plugin
deno add npm:@tailwindcss/vite
# ✅ CORRECT - Install both packages
deno add npm:@tailwindcss/vite npm:tailwindcss
@tailwindcss/vite 插件要求单独安装核心 tailwindcss 包。
Fresh Alpha 版本(2.0.0-alpha.*)
有些项目使用 Fresh 2.x alpha 版本(例如 @fresh/[email protected])。这些不是 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 |
重要: 如果你在 deno.json 中包含 @fresh/[email protected].* 的项目里看到 dev.ts,请勿将其视为 Fresh 1.x 遗留文件。它是 alpha 版本的正确入口点。在建议更改之前,请检查 deno.json 中的导入,以确定正在使用的版本。
Alpha 项目也使用处理程序模式 define.handlers({ GET(ctx) { ... } }),该模式会返回 { data: {...} }——这与稳定版 2.x 相同。
从 Fresh 1.x 迁移到 2.x
如果你已有 Fresh 1.x 项目,请运行迁移工具:
deno run -Ar jsr:@fresh/update
此工具会自动:
- 将旧导入路径转换为新的
fresh包 - 更新处理程序签名,使用单个
(ctx)参数 - 移除旧版生成文件和配置文件
- 创建
vite.config.ts和client.ts - 更新
deno.json任务以使用 Vite - 将单独的错误页面合并为统一的
_error.tsx - 将已弃用的上下文方法调用更新为 Fresh 2.x 的对应方法
手动迁移检查清单
如果工具遗漏了任何内容:
- 导入:所有 Fresh 导入都应来自
fresh或fresh/runtime - 处理程序:应使用单个
(ctx)参数,通过ctx.req访问请求 - 文件:移除所有旧版生成文件、开发入口点或旧配置文件
- 任务:更新为
vite、vite build、deno serve -A _fresh/server.js - 错误页面:使用统一的
_error.tsx - Tailwind:使用
@tailwindcss/vite(Vite 插件)