Fresh 1.2 – 欢迎全职维护者、在岛屿之间共享状态、有限的 npm 支持等等
自从我们推出 Fresh 1.0 已经快一年了,这是一个现代化的、以 Deno 为先的、边缘原生全栈 Web 框架。它拥抱工具和渐进增强方面的现代发展,使用服务器端即时渲染和使用岛屿 的客户端水合。Fresh 默认情况下向客户端发送 **0KB** 的 JavaScript。自去年以来,Fresh 已经取得了巨大的发展,成为GitHub 上星级最高的前端项目之一。
然而,一直存在一个问题——Fresh 是否是 Deno 团队真正致力于维护的东西? 当您问这个问题时,我们一直说“是的!”,但现实情况更复杂。我们从四月开始,Fresh 仓库中有超过 60 个未解决的(且未审阅的)请求——我们没有像您习惯于 Deno 运行时项目那样进行维护。这一切都归结于我没有足够的时间专注于 Fresh。
我们在去年年底看到了这一问题的最初迹象——因此我们开始寻找人选来接替我作为 Fresh 的主要维护者。长话短说,我们找到了。我非常高兴地宣布 **Marvin Hagemeister 已加入 Deno 公司,并将继续全职领导 Fresh 项目。** 如果你还不认识 Marvin:他是Preact 的维护者、Preact DevTools 的构建者,以及 JavaScript 生态系统的加速器(就在今年,他将 npm scripts
的开销从 400 毫秒降低到了 22 毫秒!)。关注他,如果你还没有的话。
Fresh 的未来比以往任何时候都更加光明。在接下来的几个月里,您可以期待在可用性、功能、性能和项目维护方面取得重大改进。我们仍在制定未来的计划路线图,一旦准备就绪,我们将与您分享。
现在,让我们深入了解 Fresh 1.2 的亮点功能
- 在岛屿属性中传递信号、Uint8Arrays 和循环数据
- 将 JSX 传递到岛屿,并在彼此之间嵌套岛屿
- 对
npm:
指定符的支持有限 - 支持自定义
HEAD
处理程序 HandlerContext.render
的状态和标题覆盖./islands
文件夹中的子目录- 异步插件渲染
- 简化 Fresh 项目的测试
要创建一个新的 Fresh 项目,请运行
$ deno run -A -r https://fresh.deno.dev my-app
要将您的项目更新到 Fresh 的最新版本,请从项目的根目录运行更新脚本
$ deno run -A -r https://fresh.deno.dev/update .
还没有安装 Deno?立即安装它。
在岛屿属性中传递信号、Uint8Arrays 和循环数据
Fresh 设计的核心是岛屿:在服务器端和客户端渲染的单个组件。(Fresh 中的所有其他 JSX 都只在服务器端渲染。)为了便于在执行初始服务器渲染后使用客户端渲染“恢复”,用户可以将属性传递到岛屿,就像他们可以将属性传递到所有其他组件一样。
从今天开始,用户除了所有现有的 JSON 可序列化值之外,还可以将循环对象、Uint8Array
或 Preact 信号传递到岛屿。这解锁了许多新的用例,例如将同一个信号传递到多个岛屿,并使用该信号在这些岛屿之间共享状态
// routes/index.tsx
import { useSignal } from "@preact/signals";
import Header from "../islands/Header.tsx";
import AddToCart from "../islands/AddToCart.tsx";
export default function Page() {
const cart = useSignal<string[]>([]);
return (
<div>
<Header cart={cart} />
<div>
<h1>Lemon</h1>
<p>A very fresh fruit.</p>
<AddToCart cart={cart} id="lemon" />
</div>
</div>
);
}
// islands/Header.tsx
import { Signal } from "@preact/signals";
export default function Header(props: { cart: Signal<string[]> }) {
return (
<header>
<span>Fruit Store</span>
<button>Open cart ({props.cart.value.length})</button>
</header>
);
}
// islands/AddToCart.tsx
import { Signal } from "@preact/signals";
export default function AddToCart(props: {
cart: Signal<string[]>;
id: string;
}) {
function add() {
props.cart.value = [...props.cart.value, id];
}
return <button onClick={add}>Add to cart</button>;
}
到目前为止,传递到岛屿的属性必须是 JSON 可序列化的,以便它们可以在服务器上序列化、通过 HTTP 发送到客户端并在浏览器中反序列化。这种 JSON 序列化意味着许多类型的对象无法序列化:例如循环结构、Uint8Array
或 Preact 信号。
将 JSX 传递到岛屿,并在彼此之间嵌套岛屿
为了更进一步,我们添加了对将 JSX 子元素传递到岛屿的支持。如果您愿意,它们甚至可以彼此嵌套。这允许您以最适合您的应用程序的方式混合动态和静态部分。
// file: /route/index.tsx
import MyIsland from "../islands/my-island.tsx";
export default function Home() {
return (
<MyIsland>
<p>This text is rendered on the server</p>
</MyIsland>
);
}
在浏览器中,我们可以仅从 HTML 中推断出 <p>
元素作为子元素传递到 MyIsland
中。这使您的网站保持精简和轻量级,因为我们不需要除需要渲染的 HTML 之外的任何其他信息。
同样地,我们现在检测到您是否将一个岛屿嵌套在另一个岛屿中。每当发生这种情况时,我们将像对待标准 Preact 组件一样对待内部岛屿。
// file: /route/index.tsx
import MyIsland from "../islands/my-island.tsx";
import OtherIsland from "../islands/other-island.tsx";
export default function Home() {
return (
<MyIsland>
<OtherIsland>
<p>This text is rendered on the server</p>
</OtherIsland>
</MyIsland>
);
}
在未来,我们希望更多地尝试允许嵌套岛屿被延迟初始化。敬请期待!
如果您有兴趣了解内部实现细节,我们建议您查看使之成为可能的拉取请求:https://github.com/denoland/fresh/pull/1285。
npm:
指定符的支持有限
对 现在在 Fresh 中支持导入 npm:
包,无论是在服务器渲染期间还是在岛屿中。不需要本地 node_modules/
文件夹来使用 npm:
指定符——就像您习惯于从 Deno 中一样。
// routes/api/is_number.tsx
import isNumber from "npm:is-number";
export const handler = {
async GET(req) {
const input = await req.json();
return Response.json(isNumber(input));
},
};
请注意,Deno Deploy *目前* 不支持 npm:
指定符,因此在将 Fresh 应用程序部署到 Deno Deploy 时不能使用它们。您可以期待 Deno Deploy 很快支持 npm:
指定符。现在,您可以在将 Fresh 部署到 VPS 或通过 Docker 部署到像Fly.io 这样的服务时使用 npm:
指定符。
HEAD
处理程序
支持自定义 现在可以在路由中为 HEAD
请求声明一个处理程序。以前,路由使用默认实现,该实现使用 GET
处理程序来处理 HEAD
请求,并省略正文。这种行为仍然有效,但可以通过为 HEAD
请求传递自定义函数来覆盖。
// routes/files/:id.tsx
export const handler = {
async HEAD(_req, ctx) {
const headers = await fileHeaders(ctx.params.id);
return new Response(null, { headers });
},
async GET(_req, ctx) {
const headers = await fileHeaders(ctx.params.id);
const body = await fileBody(ctx.params.id);
return new Response(body, { headers });
},
};
感谢 Kamil Ogórek 的贡献。
HandlerContext.render
的状态和标题覆盖
现在可以设置通过 ctx.render
创建的 Response
的状态和标题——例如,如果您想使用状态代码 400 响应 HTML 页面,您现在可以这样做
// routes/index.ts
export const handler = {
async GET(req, ctx) {
const url = new URL(req.url);
const user = url.searchParams.get("user");
if (!user) {
return ctx.render(null, {
status: 400,
headers: { "x-error": "missing user" },
});
}
return ctx.render(user);
},
};
./islands
文件夹中的子目录
以前,所有岛屿都必须在 ./islands
目录中直接声明在文件中。现在,它们可以包含在 ./islands
目录中的文件夹中。
// Always valid:
// islands/Counter.tsx
// islands/add_to_cart.tsx
// Newly valid:
// islands/cart/add.tsx
// islands/header/AccountPicker.tsx
感谢 Asher Gomez 添加了此功能。
异步插件渲染
Fresh 支持插件,插件可以自定义页面的渲染方式。例如,Twind 插件 从渲染的页面中提取 Tailwind CSS 类,并为这些类生成一个 CSS 样式表。
到目前为止,这些“渲染钩子”必须是同步的。但是,某些用例(例如使用 UnoCSS)需要异步“渲染钩子”。现在,Fresh 支持 renderAsync
钩子。
有关使用renderAsync
钩子的信息,请参阅文档:https://fresh.deno.dev/docs/concepts/plugins#hook-renderasync.
感谢Tom将此功能添加到 fresh。
简化 Fresh 项目的测试
$fresh/server.ts
现在导出一个新的 createHandler
函数,可用于从您的 Fresh 清单中创建处理程序函数,以便用于测试。
import { createHandler } from "$fresh/server.ts";
import manifest from "../fresh.gen.ts";
import { assert, assertEquals } from "$std/testing/asserts.ts";
Deno.test("/ serves HTML", async () => {
const handler = await createHandler(manifest);
const resp = await handler(new Request("http://127.0.0.1/"));
assertEquals(resp.status, 200);
assertEquals(resp.headers.get("content-type"), "text/html; charset=utf-8");
});
在文档中阅读有关为 Fresh 项目编写测试的更多信息:https://fresh.deno.dev/docs/examples/writing-tests
感谢 Octo8080X 使测试变得更容易。
下一步
我们很高兴有一个全职维护人员来改进和发展 Fresh。一如既往,如果您有任何问题,请在 Discord 中告诉我们。
不要错过任何更新 - 在 Twitter 上关注我们!