跳到主要内容
Deno 2.4 已发布,带来 deno bundle、bytes/text 导入、稳定的 OTel 等更多功能
了解更多
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。在这种模型中,大部分渲染在服务器上完成,客户端仅负责重新渲染小块交互区域。在这种模型中,开发人员明确选择对特定组件进行客户端渲染。Jason Miller 在他 2020 年的 岛屿架构 博客文章中描述了这种模型。

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

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

快速开始

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

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

此脚本会在您指定为初始化脚本最后一个参数的文件夹中(本例中为 my-app)生成 Fresh 项目所需的最少样板代码。您可以在入门指南中了解所有文件的含义。

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 的岛屿(Islands)的作用。它们是应用程序的独立组件,在客户端重新激活(re-hydrated)以允许交互。

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

// 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 个区域提供服务,免费套餐每天包含 10 万次请求。

生产就绪

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

试试 Deno Deploy——您会对其速度和简洁性感到惊讶。


最后,感谢 Sylvain Cau@hashrockChristian Norrman 在 Fresh 项目上的帮助。还要感谢 Preact 团队构建了 Preact,以及 Jason Miller岛屿架构博客文章。