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、@hashrock 和 Christian Norrman 对 Fresh 项目的帮助。另外感谢 Preact 团队 构建了 Preact,以及 Jason Miller 的 Islands Architecture 博客文章。