导言
之前笔者学前端都是浅尝辄止,只会抄个界面下来调 elementUI 这种组件库,没学过任何一个前端框架。但是笔者一直觉得前端很有意思,只是没空去学习,现在笔者决定借助大模型,一边开发一边学习 React 开发,并且基于 Deno 和 Supabase 创建个人全栈应用。
React 据说所有东西都是拆成组件的,很适合喂给 AI 慢慢看,也很适合逐步开发,而且 deno 已经完全兼容了 next.js 这经典的生态。
从零创建一个 Fresh 应用
笔者想先抄一下 vitepress 的主页,来实现一版课题组主页。
既然已经开始用 Deno 了,这边先创建一个 Fresh 框架的应用:
deno run -Ar jsr:@fresh/init
接下来我们阅读示例项目的源码:
首先从 main.ts 看起
import { App, staticFiles } from "fresh";import { define, type State } from "./utils.ts";
export const app = new App<State>();
app.use(staticFiles());
// Pass a shared value from a middlewareapp.use((ctx) => { ctx.state.test_global = "全局变量测试"; return ctx.next();});
// this can also be defined via a file. feel free to delete this!const exampleLoggerMiddleware = define.middleware(async (ctx) => { console.log(`${ctx.req.method} ${ctx.req.url}`); const res = await ctx.next(); console.log(`${ctx.req.method} ${ctx.req.url}` + `-> ${res.status}`); return res;});app.use(exampleLoggerMiddleware);
// Include file-system based routes hereapp.fsRoutes();import { createDefine } from "fresh";
// This specifies the type of "ctx.state" which is used to share// data among middlewares, layouts and routes.export interface State { state: string; test_global: string;}
export const define = createDefine<State>();这个展示了 Fresh 的一些核心特性,包括下面这些:
export const app = new App<State>();创建一个app实例,而State则是一个自定义的上下文类型,自己可以随便改,把一些全局变量存在这里就行了,不过生命周期只有一个 http 请求。这里我提供了个string的test_global方便理解。app.use(async (ctx) => {...});这里主要是介绍app.use()的用法。use()是用来添加中间件的。我们还可以用文件导入,这里用Middleware helper就好,直接define.middleware(...),这个会返回一个use()可以使用的自定义middleware。
这里的写法类似下面的
import { App, staticFiles } from "fresh";import { type State } from "./utils.ts";import { LoggerMiddleware } from "./middleware/LoggerMiddleware.ts";
export const app = new App<State>();
app.use(staticFiles());
// Pass a shared value from a middlewareapp.use((ctx) => { ctx.state.test_global = "全局变量测试"; return ctx.next();});
app.use(LoggerMiddleware);
// Include file-system based routes hereapp.fsRoutes();import { define } from "../utils.ts";
// this can also be defined via a file. feel free to delete this!export const LoggerMiddleware = define.middleware(async (ctx) => { console.log(`${ctx.req.method} ${ctx.req.url}`); const res = await ctx.next(); console.log(`${ctx.req.method} ${ctx.req.url}` + `-> ${res.status}`); return res;});OK,接下来看我们经典的 index.tsx,因为本质上是 fsRoute() 所以这里实际上很方便,在 route 目录下找到就好了。不过 Fresh 有一个很神奇的东西,叫 App wrapper,这里先看
我们先看 _app.tsx, 他相当于一个 App wrapper ,也就是对全局的一个包裹,不过从 fs 的角度来说,似乎是 _app.tsx 这种用法更自然。
import { define } from "../utils.ts";
export default define.page(function App({ Component }) { return ( <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>uestc-ndsl-hpc-web</title> </head> <body> <Component /> </body> </html> );});随后我们阅读 index.tsx:
import { useSignal } from "@preact/signals";import { Head } from "fresh/runtime";import { define } from "../utils.ts";import Counter from "../islands/Counter.tsx";
export default define.page(function Home(ctx) { const count = useSignal(3);
console.log("Shared value " + ctx.state.shared);
return ( <div class="px-4 py-8 mx-auto fresh-gradient min-h-screen"> <Head> <title>Fresh counter</title> </Head> <div class="max-w-3xl mx-auto flex flex-col items-center justify-center"> <img class="my-6" src="/logo.svg" width="128" height="128" alt="the Fresh logo: a sliced lemon dripping with juice" /> <h1 class="text-4xl font-bold">Welcome to Fresh</h1> <p class="my-4"> Try updating this message in the <code class="mx-2">./routes/index.tsx</code> file, and refresh. </p> <Counter count={count} /> </div> </div> );});这个展示了 Fresh 的一些核心特性,包括下面这些:
export default define.page(function Home(ctx) { ... });是用来给页面提供类型提示的,避免只有在浏览器运行、用户点击时才报错。Home则是一个function在前端可能以组件的想法更合适。const count = useSignal(3);类似声明一个局部变量,更准确的说是一个带自动通知机制的响应式变量,可以用数据表示状态。console.log则是经典的控制台日志,前端第一课。return(...)看起来像返回的是html实际上这里返回的是一个jsx,是react的写法。
下面再看一下其他的几个 fresh 提供的 define 的属性
import { define } from "../utils.ts";
export default define.page(function App({ Component }) { return ( <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>uestc-ndsl-hpc-web</title> </head> <body> <Component /> </body> </html> );});这里相当于提供了这个文件代表的界面。
看完了这些,基本上对于 fresh 如何编写有了初步的认识,接下来笔者将会设计一个 Navbar 的场景,然后利用 fresh 的 layout 特性。
Navbar 的设计和应用
我们接下来设计一下 navbar,当然我们不能完全自己写,那么很多操作写起来很麻烦。
先按官方的教程导入我们的 daisyui:
deno i -D npm:daisyui@latest不过现在的 style.css 在 asset 目录下。
@import "tailwindcss"; @plugin "daisyui";现在我们就可以用 daisyui 的组件了。
我们设计一个 logo ,一个黑夜模式按钮加上一个 menu 菜单如下
import { asset } from "fresh/runtime";import { MdModeNight } from "@preact-icons/md";
interface NavbarProps { text?: string; logo_url?: string; navItems?: string[]; home_click?: () => void; dark_mode_click?: () => void;}
export default function Navbar({ text = "Navbar", logo_url = asset("img/avatar-test.jpg"), navItems = ["Item1", "Item 2", "Item 3"], home_click, dark_mode_click,}: NavbarProps) { return ( <div className="navbar bg-primary text-primary-content rounded-box justify-between"> {/* Logo */} <button type="button" className="btn btn-xs sm:btn-xs md:btn-sm lg:btn-md xl:btn-lg btn-ghost text-md btn-secondary" onClick={home_click} > <div className="avatar"> <div className="w-10 rounded-full"> <img src={`${logo_url}`} alt="Logo" /> </div> </div> {text} </button>
{/* 右侧容器:导航和黑夜模式 */} <div className="flex items-center gap-4"> {/* 导航菜单 */} <ul className="menu menu-xs sm:menu-xs md:menu-sm lg:menu-md xl:menu-lg menu-horizontal menu-ghost rounded-box text-md"> {navItems.map((item, index) => ( <li key={index}> <a>{item}</a> </li> ))} </ul>
{/* 黑夜模式 */} <button type="button" className="btn btn-xs sm:btn-sm md:btn-md lg:btn-lg xl:btn-xl btn-ghost btn-circle btn-secondary" onClick={dark_mode_click} > <MdModeNight /> </button> </div> </div> );}稍做改动,迁移到 _layout.tsx 下:
import { define } from "../utils.ts";import { asset } from "fresh/runtime";import Navbar from "../components/navbar.tsx";
export default define.page(function Home({ Component }) { return ( <div class="px-4 py-8 mx-auto min-h-screen bg-base-100"> <Navbar text="UESTC NDSL HPC" navItems={["Home", "Team", "Pub"]} logo_url={asset("img/logo.png")} /> <Component /> </div> );});最后的效果如下

接下来我们可以对标 vitepress 写一个 Hero 组件,初步掌握这个怎么写以后可以借助 AI 的力量了。我们实现了下面的 Hero.tsx 组件
import { asset } from "fresh/runtime";
interface HeroProps { title?: string; subtitle?: string; tagline?: string; logo_url?: string; button_accent_text?: string; button_secondary_text?: string; cards?: { card_title: string; card_text: string }[];}
export default function Hero({ title = "UESTC NDSL HPC", subtitle = "A group led by Dr. Zhang focusing on High Performance Computing and Numerical Linear Algebra.", tagline = "University of Electronic Science and Technology of China", logo_url = asset("img/logo.png"), button_accent_text = "Learn More", button_secondary_text = "Contact Us", cards = [ { card_title: "Cookies!", card_text: "We are using cookies for no reason." }, { card_title: "Cookies!", card_text: "We are using cookies for no reason." }, { card_title: "Cookies!", card_text: "We are using cookies for no reason." }, ],}: HeroProps) { return ( <div> <div className="w-full h-auto mx-auto bg-base-200 text-base-content rounded-box my-8"> <div className="hero-content gap-10 flex-col lg:flex-row items-center"> {/* Left: Text */} <div className="flex-1 max-w-3xl"> <p className="sm:text-3xl md:text-4xl lg:text-5xl font-semibold tracking-tight bg-linear-to-r from-primary/80 to-secondary/80 bg-clip-text text-transparent"> {title} </p> <p className="mt-4 sm:text-xl md:text-2xl lg:text-3xl font-extrabold leading-[1.05]"> {subtitle} </p> <p className="mt-6 text-base opacity-70">{tagline}</p> <div className="mt-6 flex gap-4"> <button type="button" className="btn btn-accent text-accent-content px-6 py-2 rounded-lg" > {button_accent_text} </button> <button type="button" className="btn btn-secondary text-secondary-content px-6 py-2 rounded-lg" > {button_secondary_text} </button> </div> </div>
{/* Right: Image */} <div className="w-full h-auto xs:w-3xs sm:w-2xs md:w-xs rounded-lg overflow-hidden bg-base-300 mx-auto lg:mx-0"> <img src={`${logo_url}`} alt="Logo" className="w-full h-auto object-contain p-6" // No shadow here, just rounded corners /> </div> </div> </div> {/* Cards Section */} <div className="flex justify-center gap-4"> {cards.map((card, index) => ( <div key={index} className="card bg-neutral text-neutral-content w-96"> <div className="card-body items-center text-center"> <h2 className="card-title">{card.card_title}</h2> <p>{card.card_text}</p> </div> </div> ))} </div>
</div> );}效果如图

再增加一个 pub 组件,效果如下:

效果喜人啊~
接下来我要实现主题切换和 Recent Publication 从 api 接口读取(从而方便加个后端来修改)。这就要用到 island,就之后再写了。