跳到主要内容
Deno 2.4 已发布,带来 deno bundle、字节/文本导入、OTel 稳定版等功能
了解更多
Fresh lemon

Fresh 1.4 – 更快的页面加载、布局等

在此开发周期中,我们专注于整体开发者体验,让 Fresh 中使用共享布局、路由特有 Islands 等功能变得更简单。

提醒:你可以通过在项目文件夹中运行 deno run -A -r https://fresh.deno.dev 来启动一个新 Fresh 项目,或运行 deno run -A -r https://fresh.deno.dev/update . 来更新现有项目。

通过提前编译实现更快的页面加载

到目前为止,Fresh 始终是即时编译资源。这在之前一直运行良好,因为它实现了无构建步骤的极速部署。但我们意识到,对大型 Islands 进行即时编译(=JIT)渲染会明显变慢。我们最终找到了一个预编译解决方案,它使得无服务器函数在冷启动时资源服务速度提升约 45-60 倍,同时对部署时间影响极小。节省的效果取决于 Island 的大小,但即使是小型 Islands,改进也十分显著。

以下是 Fresh 文档网站上不同技术的演示。搜索框是一个小型 Island,大小约为 30kB。

使用提前编译的资源,搜索框 Island 几乎可以即时恢复,而使用 JIT 编译则需要 4.28 秒。

在本地运行开发服务器时,Fresh 将始终使用 JIT 编译,这样你的服务器可以尽快响应 API 请求,无需等待资源编译完成。

你可以遵循提前构建指南,选择在部署时进行 AOT 编译。运行 deno task build 将创建一个 _fresh 文件夹,其中包含所有生成的资源。

自定义 html、head 和 body 标签

在之前的版本中,一个棘手的问题是如何在 <html> 标签上设置 lang 属性。在此之前,Fresh 在内部创建了直到 <body> 标签的外部 HTML 结构,你需要使用变通方法,例如创建自定义渲染函数来修改 lang 属性。

await start(manifest, {
  // Old way of setting the `lang` attribute,
  // requires a custom render function :(
  render: (ctx, render) => {
    ctx.lang = "de";
    render();
  },
});

经过一番思考,我们意识到通过允许你自己渲染 HTML 文档,可以大大简化 Fresh。所以我们正是这样做的。使用 Fresh 1.4,你可以直接在服务器上设置 <html><head><body> 标签。

// routes/_app.tsx
import { AppProps } from "$fresh/server.ts";

export default function App({ Component }: AppProps) {
  return (
    <html lang="de">
      <head>
        <title>My Fresh App</title>
      </head>
      <body>
        <Component />
      </body>
    </html>
  );
}

为了使从旧版 Fresh 升级更容易,我们添加了一些逻辑来检测是否渲染了 <html> 标签。如果没有渲染,我们将像过去 Fresh 版本中那样,回退到使用内部模板包装 HTML。

布局

构建 Web 应用的一个常见方面是,许多布局部分在不同路由之间共享。例如网站的页眉或页脚,它们在所有路由中都是相同的组件。以前你可以在 routes/_app.tsx 中实现这一点,但无法在此基础上进一步。为应用中的某些子路由创建共享布局需要将代码提取到组件中,并手动将其导入到所有路由。

在 Fresh 1.4 中,我们增加了对 _layout 文件的支持,可以将其描述为路由本地的应用封装器。它们可以放置在任何路由文件夹中,Fresh 将检测所有匹配的布局并将它们堆叠在一起。

routes/
  _app.tsx
  _layout.tsx
  page.tsx      # Inherits _app and _layout

  sub-route/
    _layout.tsx # Inherits _app and _layout
    index.tsx   # Inherits _app, _layout and sub-route/_layout
    about.tsx   # Inherits _app, _layout and sub-route/_layout

一个 _layout 文件看起来与路由文件或应用封装器非常相似。它使用 Component prop 来继续渲染后续布局或最终路由文件。

// routes/_layout.tsx
import { LayoutProps } from "$fresh/server.ts";

export default function MyLayout({ Component }: LayoutProps) {
  return (
    <div class="my-layout">
      <h2>This is rendered by a layout</h2>
      <Component />
    </div>
  );
}

但有时你可能想继承布局甚至应用封装器,因此我们也增加了一种让你选择退出的方式。

export const config: RouteConfig = {
  skipAppWrapper: true, // Disable rendering app wrapper
  skipInheritedLayouts: true, // Disable already inherited _layout templates
};

感谢 Michael Gearhardt 启动了此功能的工作!

异步布局和异步应用封装器

一旦我们实现了对布局的支持,我们就在想,如果让它们与路由一样会怎样?我们能否也允许异步布局组件呢?如果路由组件和布局组件的行为方式相同,肯定会减少心智负担。经过一番编码,我们发现这是可行的。因此,本次 Fresh 发布不仅带来了 _layout 组件,还带来了异步布局!

export default async function Layout(req: Request, ctx: LayoutContext) {
  const person = await fetchSomeData();

  return (
    <div>
      <h1>Hello {person.name}</h1>
      <ctx.Component />
    </div>
  );
}

既然如此,为什么不让应用封装器也异步呢?在 Fresh 1.4 中,所有布局的行为都相同。唯一的特例是路由(Routes),因为它们处于渲染链的末端,因此没有 ctx.Component 属性。

使用定义函数提高输入效率

随着异步路由组件的引入,我们收到一些反馈,认为函数定义需要太多关键字,变得有点“冗长”。

export default async function Page(req: Request, ctx: RouteContext) {
  // ...
}

对此我们也有同感。我注意到每次输入这些内容都需要一点时间。当然,可以在编辑器中添加自定义代码片段来创建这些样板代码,但这更像是一种变通方法,而非真正的解决方案。

因此,我们花了一些时间反复讨论,最终提出了 define* 辅助函数的概念。它们不包含任何逻辑,但开箱即用,可为编辑器提供自动补全提示,无需你自己定义类型。

// Both `req` and `ctx` will have the correct type already
export default defineRoute(async (req, ctx) => {
  // ...
}

如果你查看这两个代码片段,似乎没有太大区别。但当你在编辑器中实际输入时,你会发现后者输入起来要快得多。

以下定义辅助函数可用:

  • defineRoute 用于创建路由
  • defineLayout 用于创建布局
  • defineApp 用于创建应用封装器。

使用路由组组织代码

通常,routes/ 目录中的嵌套文件夹直接映射到 URL。然而,对于大型项目,常常会出现你希望对文件进行分组,但又不希望这影响 URL 结构的情况。

路由组使这成为可能。路由组是 routes/ 内部的一个文件夹,其名称被括号包围,例如 (my-group)。这还允许你为同一分段上的路由拥有不同的 _layout_middleware 文件。

routes/
  (marketing)/
    _layout.tsx
    about.tsx  # Maps to /about

  (blog)/
    _layout.tsx
    archive.tsx # Maps to /archive

协同定位 Islands、组件及更多

当路由组文件夹的名称以下划线开头时,例如 (_components),Fresh 将忽略该文件夹,并将其视为私有。这意味着你可以使用这些私有路由文件夹来存储与特定路由相关的组件。其中一个特殊名称是 (_islands),它告诉 Fresh 将该文件夹中的所有文件视为一个 Island。

routes/
  shop/
    (_components)/  # ignored by the router
      Section.tsx

    (_islands)/     # local islands folder
      Cart.tsx

    index.tsx

将它们结合起来,这使你能够基于功能组织代码,并将所有相关的组件、Islands 或其他任何内容放入一个共享文件夹中。

未来展望?

还有许多正在开发中的功能未能在此次发布中包含,因为它们还需要一些时间来完善。特别是,我们正在彻底改进我们的插件系统,使其更易理解且功能更强大。添加视图过渡支持的 PR 进展顺利,我们正在此基础上探索如何在 Fresh 中添加类 SPA 的客户端导航。我们还在关注其他样式解决方案,如 UnoCSS、直接使用 Tailwind 等。

和上个月一样,你可以在 GitHub 上关注本月的迭代计划

你知道吗?Deno 1.36 刚刚发布了。

请务必查阅 Deno 1.36 的发布说明,它带来了改进的安全控制、测试、性能基准测试等功能。