自从我们推出 Fresh 1.0 以来已经快一年了,这是一个现代的、Deno 优先的、边缘原生的全栈 Web 框架。它采用现代的工具开发和渐进式增强,使用服务器端实时渲染和使用客户端 Islands 的架构。默认情况下,Fresh 会向客户端发送 0KB 的 JavaScript。自去年以来,Fresh取得了巨大的增长,成为GitHub上最热门的前端项目之一。

然而,房间里有一头大象 - Fresh 是 Deno团队真正致力于维护的东西吗?当你问的时候,我们总是说“是的!”,但现实要复杂得多。从 4 月开始,我们在 Fresh repo 上发布了 60 多个未完成(且未经审查)的 Pull Request - 我们没有跟上 Deno 运行时项目所习惯的维护水平。这在很大程度上归结为没有足够的时间专注于 Fresh。

我们在去年年底左右看到了这方面的初步迹象 - 所以我们开始寻找某人来取代我成为 Fresh 的主要维护者。长话短说,发现我们做到了。我欣喜若狂地宣布,Marvin Hagemeister加入了Deno公司,并将全职领导Fresh项目向前发展。如果你还不知道Marvin是谁:他是Preact的维护者,Preact DevTools的构建者,以及JavaScript生态系统的加速者(就在今年,他将npm scripts 的开销从400ms加速到22ms!)。如果你还没有关注他,请关注他。

Fresh的未来看起来比以往任何时候都更加光明。在接下来的几个月中,您可以期待可用性、功能、性能和项目维护方面的重大改进。我们仍在为未来的计划制定确切的路线图,一旦准备就绪,我们将分享。

现在,让我们深入了解 Fresh 1.2 的亮点功能.

若要创建新的全新项目,请运行:

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

若要将项目更新到最新版本的 Fresh,请从项目的根目录运行更新脚本:

$ deno run -A -r https://fresh.deno.dev/update .

还没有安装 Deno?立即安装。

在 Islands 道具中传递信号、Uint8Array 和循环引用数据

Fresh设计的核心是 Islands :在服务器和客户端上渲染的各个组件。(Fresh 中的所有其他 JSX 都只在服务器上呈现)。为了在执行初始服务器渲染后轻松“恢复”到客户端渲染,用户可以将 props 传递给 Islands ,就像处理所有其他组件一样。

从今天开始,除了所有现有的 JSON 可序列化值之外,用户还可以将循环对象 Uint8Array 或 Preact 信号传递给 Islands 。这解锁了一系列新的用例,例如将相同的信号传递给多个 Islands ,并使用该信号在这些 Islands 之间共享状态:

// 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>;
}

到目前为止,传递给 Islands 的 props 必须是可 JSON 序列化的,这样它们就可以在服务器上序列化,通过 HTTP 发送到客户端,并在浏览器中反序列化。这种 JSON 序列化意味着无法序列化许多类型的对象:例如循环结构或 Uint8Array Preact 信号。

将 JSX 传递到Islands 以及内部嵌套的 Islands

为了做得更好,我们添加了对将 JSX 子项传递到Islands的支持。如果您愿意,它们甚至可以相互嵌套。这允许你以最适合你的应用的方式混合动态和静态部分。

// 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>
  );
}

在浏览器中,我们可以推断出 <p> element 是作为子元素仅从 HTML 传递给 的 MyIsland 。这使您的网站保持精简和轻量级,因为除了我们需要呈现的 HTML 之外,我们不需要任何其他信息。

同样,我们现在会检测您何时在另一个Islands中构建。每当发生这种情况时,我们都会将内岛视为标准 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>
  );
}

将来,我们希望更多地尝试允许延迟初始化嵌套Islands。敬请期待!

如果您对内部实现细节感兴趣,我们建议您查看使其成为可能的 PR:https://github.com/denoland/fresh/pull/1285

npm: 说明符的支持有限

Fresh 现在支持导入 npm: 包,无论是在服务器渲染期间还是针对 Islands 。使用 npm: 说明符不需要本地 node_modules/ 文件夹 - 就像您习惯的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 部署时无法使用说明符。您可以期待 npm: 很快在Deno Deploy中支持说明符。目前,您可以在将 Fresh 部署到 VPS 或通过 Docker 部署到 Fly.io 等服务时使用 npm: 说明符。

支持自定义 HEAD 处理程序

现在可以为路由中的请求声明 HEAD 处理程序。以前,路由使用带有 HEAD 请求 GET 处理程序的默认实现,省略了正文。此行为仍然有效,但可以通过为请求传递 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 });
  },
};

感谢卡米尔·奥戈雷克的贡献。

HandlerContext.render的状态 和标头覆盖

现在可以设置 Response 创建方式 ctx.render 的状态和标头 - 例如,如果您想使用状态代码为 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 目录中的文件中声明。现在,它们可以包含在 ./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

谢谢汤姆把它添加到 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 使测试更容易。