跳到主要内容
Deno dinosaur drinking a fresh lemonade

Fresh 1.0

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

近年来,客户端渲染变得越来越流行。React(以及类似 React 的)页面允许程序相对轻松地构建非常复杂的 UI。这种流行性在当前的 Web 框架领域中得到了体现:它由基于 React 的框架主导。

但是客户端渲染非常昂贵;框架通常会在每次请求时向用户发送数百 KB 的客户端 JavaScript。这些 JS 包通常只是渲染静态内容,而这些静态内容完全可以作为纯 HTML 提供。

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

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

Fresh 使用了不同的模型:默认情况下,向客户端发送 0 KB 的 JS。大部分渲染在服务器上完成,客户端仅负责重新渲染少量交互岛屿。开发者可以显式选择为特定组件进行客户端渲染的模型。Jason Miller 在 2020 年的 Islands Architecture 博客文章中描述了这种模型。

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 的用途。它们是应用程序的各个组件,在客户端上重新水合以允许交互。

下面是一个 island 的示例,它提供了一个带有递增和递减按钮的客户端计数器。它使用 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 必须放置在 islands/ 文件夹中。Fresh 负责在客户端上自动重新水合 island,如果在路由的模板中遇到它的使用。

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 MillerIslands Architecture 博客文章。