跳到主要内容
Deno 2 终于来了 🎉️
了解更多
Deno dinosaur drinking a fresh lemonade

Fresh 1.0

Fresh 是一个全新的 Deno 全栈 Web 框架。默认情况下,使用 Fresh 构建的网页不会向客户端发送任何 JavaScript。该框架没有构建步骤,这使得部署时间提高了一个数量级。今天我们发布了 Fresh 的第一个稳定版本。

近年来,客户端渲染变得越来越流行。React(以及类似 React 的页面)允许程序以相对容易的方式构建非常复杂的 UI。其流行程度在当前的 Web 框架领域中表现明显:它被基于 React 的框架所主导。

但客户端渲染成本高昂;框架通常会在每次请求时向用户发送数百千字节的客户端 JavaScript。这些 JS 包通常除了渲染静态内容之外几乎没有其他作用,而这些内容本可以作为纯 HTML 提供。

一些更新的框架也支持服务器端渲染。这通过在服务器上预渲染来帮助减少页面加载时间。但大多数当前的实现仍然将整个渲染基础架构发送到每个客户端,以便页面可以在客户端上完全重新渲染。

这是一个糟糕的发展趋势 - 客户端 JavaScript 真的很昂贵:它会减慢用户体验速度,大幅增加移动设备的功耗,并且通常不够健壮。

Fresh 使用了不同的模型:一个默认情况下向客户端发送 **0 KB** JS 的模型。一个大多数渲染在服务器上完成,客户端只负责重新渲染少量交互岛的模型。一个开发人员可以明确选择针对特定组件进行客户端渲染的模型。这个模型在 2020 年由 Jason Miller 在他的 Islands Architecture 博客文章 中描述过。

从本质上讲,Fresh 是一个路由框架和模板引擎,它在服务器上按请求渲染页面。除了在服务器上进行即时 (JIT) 渲染之外,Fresh 还提供了一个接口,用于无缝地在客户端上渲染一些组件以实现最大的交互性。该框架使用 Preact 和 JSX(或 TSX)在服务器和客户端上进行渲染和模板化。客户端渲染完全是按组件级别进行的,因此许多应用程序根本不会向客户端发送任何 JavaScript。

Fresh 没有构建步骤。您编写的代码直接就是运行在服务器和客户端的代码,任何必要的 TypeScript 或 JSX 到纯 JavaScript 的转译都是即时完成的,在需要时进行。这使得迭代循环非常快,并实现 **即时部署**。

快速入门

为了真正解释 Fresh 的特别之处,让我们搭建一个新项目并查看一些代码

deno run -A -r https://fresh.deno.dev my-app

此脚本生成 Fresh 项目所需的最小样板,该项目位于您在初始化脚本中指定的最后一个参数 (在本例中为 my-app) 的文件夹中。您可以在 入门指南 中了解有关所有文件的含义的更多信息。

my-app/
├── README.md
├── deno.json
├── dev.ts
├── fresh.gen.ts
├── import_map.json
├── islands
│   └── Counter.tsx
├── main.ts
├── routes
│   ├── [name].tsx
│   ├── api
│   │   └── joke.ts
│   └── index.tsx
└── static
    ├── favicon.ico
    └── logo.svg

现在,请将您的注意力转向 routes/ 文件夹。它包含应用程序每个路由的处理程序和模板。每个文件的名称定义了路由匹配的路径。例如,api/joke.ts 文件会处理对 /api/joke 的请求。文件夹结构可能会让您想起 Next.js 或 PHP,因为这些系统也使用 文件系统路由

让我们看一下 routes/index.tsx 文件

import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div>
      <img
        src="/logo.svg"
        height="100px"
        alt="the fresh logo: a sliced lemon dripping with juice"
      />
      <p>
        Welcome to `fresh`. Try update this message in the ./routes/index.tsx
        file, and refresh.
      </p>
      <Counter start={3} />
    </div>
  );
}

路由的默认导出是 JSX 模板,该模板会在每次请求时在服务器端渲染。模板组件本身永远不会在客户端上渲染。

这就引出一个问题:如果想要在客户端上重新渲染应用程序的某些部分,例如响应某些用户交互,该怎么办?这就是 Fresh 的 岛屿 的作用。它们是应用程序的单个组件,在客户端上重新水合,以允许交互。

下面是一个岛屿的示例,它提供了一个带有递增和递减按钮的客户端计数器。它使用 Preact 的 useState 钩子来跟踪计数器值。

// islands/Counter.tsx

import { useState } from "preact/hooks";

interface CounterProps {
  start: number;
}

export default function Counter(props: CounterProps) {
  const [count, setCount] = useState(props.start);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

岛屿必须放在 islands/ 文件夹中。如果 Fresh 在路由模板中遇到岛屿的使用,它会自动在客户端上重新水合岛屿。

Fresh 不仅仅是一个前端框架,而是一个用于编写网站的完全集成的系统。您可以任意处理任何类型的请求,返回自定义响应,进行数据库请求等等。例如,此路由会返回一个纯文本 HTTP 响应,而不是 HTML 页面

// routes/api/joke.ts

const JOKES = [/** jokes here */];

export const handler = (_req: Request): Response => {
  const randomIndex = Math.floor(Math.random() * JOKES.length);
  const body = JOKES[randomIndex];
  return new Response(body);
};

这也可用于对路由进行异步数据获取。这是一个从磁盘上的文件加载博客文章的路由

// routes/blog/[id].tsx

import { HandlerContext, PageProps } from "$fresh/server.ts";

export const handler = async (_req: Request, ctx: HandlerContext): Response => {
  const body = await Deno.readTextFile(`./posts/${ctx.params.id}.md`);
  return ctx.render({ body });
};

export default function BlogPostPage(props: PageProps) {
  const { body } = props.data;
  // ...
}

由于 Fresh 非常依赖动态服务器端渲染,因此速度至关重要。这使得 Fresh 非常适合在像 Deno Deploy、Netlify Edge Functions 或 Supabase Edge Functions 这样的运行时中运行。这样可以使渲染发生在物理上靠近用户的位置,从而最大程度地减少网络延迟。

将 Fresh 应用程序部署到 Deno Deploy 只需几秒钟:将代码推送到 GitHub 存储库,然后将此存储库链接到 Deno Deploy 仪表板中的一个项目。您的项目将从全球 35 个区域提供服务,免费套餐包含每天 100k 个请求。

生产就绪

Fresh 1.0 是一个稳定版本,可以用于生产环境。Deno 的许多公共 Web 服务都使用 Fresh(例如您正在阅读此博客文章的网站!)。但这并不意味着我们已经完成了 Fresh 的开发。我们还有很多想法来改善用户和开发人员体验。

试试 Deno Deploy - 您会对它的速度和简便性感到惊讶。


最后,感谢 Sylvain Cau@hashrockChristian Norrman 对 Fresh 项目的帮助。另外感谢 Preact 团队 构建了 Preact,以及 Jason MillerIslands Architecture 博客文章。