这个比 Next.js 强多了(TanStack Server Components)

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00React 服务器组件。无论你喜欢还是讨厌它们。最近似乎大多数人都在讨厌它们,但这
00:00:04情况可能即将改变,因为 TanStack 也加入了这场游戏。没错,现在我们有了 TanStack
00:00:08服务器组件,它们采用了与 Next.js 完全不同的方法。让我们来看看。
00:00:13首先,我将引用他们公告文章中的一段话,我认为这段话会让
00:00:21很多人感到轻松。它写道:“大多数人现在以服务器优先的方式
00:00:26思考 React 服务器组件。服务器拥有这棵树,useClient 标记交互部分,框架约定
00:00:31决定了整个架构如何组合在一起。这使得 React 服务器组件从一个有用的
00:00:35原语变成了一个你的整个应用都必须围绕它运行的东西。我们认为你不应该
00:00:40为了从 React 服务器组件中获得价值,而被迫一开始就接受那种完整的模型。”
00:00:45本质上他们是在说,他们不想走 Next.js 那条路,即
00:00:48默认情况下是服务器组件,然后在需要交互的地方才使用 useClient 指令。
00:00:52相反,TanStack 想要思考的是:如果你能像在客户端获取 JSON 一样
00:00:57颗粒化地使用 React 服务器组件会怎样?带着这个目标,让我们来看看
00:01:01他们是如何真正实现服务器组件的,因为剧透预警,我真的很喜欢他们
00:01:06这样做的方式。我这里有一个普通的 TanStack Start 应用程序,所以目前所有东西
00:01:10都将是一个客户端组件,我唯一做的事情就是进行了一些简单安装步骤,你需要这些步骤来让
00:01:15服务器组件运行,这本质上只是安装一些包并修改你的
00:01:18vite.config。这就是页面目前的样子,我们这里的 greeting 组件
00:01:22应该是一个客户端组件,在代码中它确实只是一个简单的 React 组件,
00:01:27然后在下面我们有一个普通的 TanStack 路由,我们正在这里使用 greeting 组件。现在假设
00:01:32在我们的 greeting 组件中,你想在服务器上执行一些逻辑。就我而言,我想获取
00:01:36操作系统的 hostname,然后是一些仅对服务器可用的环境变量,
00:01:40仅仅是为了向你展示它确实是在那里运行的。目前如果我尝试使用 os.hostname
00:01:45它将无法工作,因为这是一个 node 函数,在浏览器中不可用。
00:01:49那么我们需要做的是,将我们的 greeting 组件放在服务器上渲染,在
00:01:53TanStack Start 中执行此操作的第一步是使用一个简单的 server 函数。正如你所看到的,我
00:01:58这里有一个叫 getGreeting 的函数,我们在里面所做的就是使用新的 renderServerComponent
00:02:01函数,把我们的组件放进去,并返回我们得到的那个可渲染的服务器组件。
00:02:06你可以认为这就像我们为组件创建了一个 GET 请求一样简单。
00:02:10接下来,我们需要做的就是简单地从我们创建的服务器函数中获取组件,
00:02:14我们可以在路由的 loader 里面这样做,所以我们只需要 await getGreeting,然后
00:02:18返回它,它依然是一个可渲染的服务器组件。然后我们就可以在
00:02:23我们的路由中使用它,通过 useLoaderData,我们只需像这样在下面使用该组件。有了这个,
00:02:27我们现在就有了第一个 TanStack 服务器组件。你可以看到 os.hostname 现在可以工作了,它也
00:02:32正在拉取仅在服务器上可用的环境变量。现在让我
00:02:36绝对喜爱这种实现方式的一点是,如果你注意到,这里唯一的新东西就是 renderServerComponent
00:02:41函数。其余的只是普通的 TanStack Start。你可以用
00:02:46返回的简单 JSON 数据替换它,你会以完全相同的方式获取它。我也认为
00:02:51这种实现方式非常明确地说明了你的代码到底是在哪里运行的。
00:02:55你所有的操作都在服务器函数内部完成,所以很清楚它将在服务器上运行,而且
00:02:59在 renderServerComponent 内部也是如此。事实上,我认为我可以通过简单地
00:03:03获取我在上面运行的、想要在服务器上运行的函数,把它们
00:03:07放到 server 函数中,然后简单地将值作为 props 传给我的 greeting 组件,从而改进我的演示代码,
00:03:12所以现在我的 greeting 组件本质上只是一个可以在
00:03:16客户端或服务器上使用的纯组件。我正在我的
00:03:21server 函数中运行所有我想要在服务器上运行的逻辑。同样,这非常明确地说明了它将在服务器上运行。
00:03:25这感觉完全是 Next.js 所用逻辑的反面,我绝对喜欢它。
00:03:30这意味着我可以以正常的方式思考 React,它首先是客户端,然后在需要时
00:03:34在其上添加服务器组件。但是,如果我想在我的服务器
00:03:38组件中使用客户端组件,也就是嵌套在树中呢?假设我想在这里添加一个计数器按钮,每次
00:03:43点击它就会增加计数。好吧,如果我尝试在我的服务器组件中添加它,带有
00:03:47一个 onClick 和一个 useState 调用,你可以看到它完全崩溃了。它说 useState 不是
00:03:52一个函数,或者其返回值不可迭代,那是因为 greeting 被用作服务器
00:03:56组件。我们不能使用此客户端功能。要修复这个问题,你有两个选择,第二个
00:04:01选项绝对是最好的,但第一个选项对于那些
00:04:05使用过 Next.js 的人来说会感觉很熟悉。我们可以简单地将该逻辑移到它自己的组件中,然后使用
00:04:10useClient 指令。这在 TanStack Start 服务器组件中有效,我们现在可以简单地在
00:04:14服务器组件中使用该组件,正如你所看到的,一切运行良好。这种方法
00:04:18的缺点是,我们现在有一个服务器组件在控制
00:04:22客户端组件的渲染,这可能会开始变得有点混乱,也就是说,如果我想找出我的
00:04:28counter 组件在树中的位置,我会转到我的路由,看到我们有一个 greeting 服务器
00:04:32组件,然后回到 greeting 服务器组件,在服务器组件内部看到
00:04:37我们有一个客户端组件,这种混乱真的会开始累积,并且让服务器和客户端的界限
00:04:42变得有点不清晰。但 TanStack 不会满足于此。他们
00:04:47问自己,如果服务器根本不需要决定每一个客户端形状的 UI 部分会怎样?
00:04:51这引导他们创造了一种全新的东西——复合组件 (Composite Components)。要使用一个复合组件,第一步
00:04:56我要做的是从我的服务器组件中移除我们的客户端组件,并简单地用
00:05:00greeting 组件的任何子元素来替换它。接下来我们还需要更改我们从
00:05:05服务器函数返回的内容。不再返回可渲染的服务器组件,我们需要返回
00:05:09所谓的复合源 (Composite Source)。为此,我们可以使用我们的第二个 TanStack 服务器组件辅助
00:05:14函数——createCompositeComponent。在其中,我们本质上只是构建一个组件,
00:05:18这里的 props 被视为插槽。我只是使用一个非常简单的 children 插槽,所以它会将
00:05:22我作为复合组件的子元素传递的任何内容,传递到 props.children 中,我将其
00:05:27传递给刚才的 greeting 组件。有了这个,我们同样需要做的就是
00:05:31简单地从我们的服务器函数中获取我们的复合组件,我们在
00:05:36loader 中以完全相同的方式执行此操作。你看到我只是将 source 重命名为 greeting,然后用 useLoaderData 加载它。
00:05:41唯一的区别是,我们不能将其用作组件。你看,它在这里抛出了一个
00:05:45错误。要实际渲染复合组件,我们需要使用从 TanStack 服务器组件获取的
00:05:49CompositeComponent 辅助组件,作为 source 属性,我们传入
00:05:53我们从之前创建的服务器函数中获取的复合组件。
00:05:57有了这个,我的服务器组件现在就像之前一样进行渲染,我也将
00:06:01计数器作为这个复合组件的子元素传递,并且它被传递给 greeting,我们在
00:06:05那里设置了它,所以它将其传递到 props.children,一切现在运行
00:06:10良好。我还可以进入我的 counter 组件并移除我们之前的指令,因为
00:06:14由于它是在复合组件内部,它知道这是一个客户端组件,所以不再需要该指令。
00:06:18这是一个被用作插槽的组件。现在,这看起来可能和仅仅使用 useClient 指令
00:06:23得到了完全相同的结果,而且工作量更大,但真正的威力来自于
00:06:27开发者体验,这与 useClient 模型有很大的不同。我们没有让
00:06:32服务器决定我们的客户端组件在哪里渲染(就像我们将 counter 组件放在
00:06:36服务器组件内部时那样),相反,我们用复合组件所做的是:说嘿,
00:06:40这里将有一个插槽,我们将渲染一个客户端组件,但服务器
00:06:44组件本身不知道那会是什么。我们在后续的客户端代码中添加它,
00:06:48所以我们在客户端代码本身中处理所有基于客户端的组件。这也仅仅是个开始,
00:06:53如果我们看一下像这个 post 页面这样更复杂的页面,这个 post 是在服务器端渲染的,
00:06:58我有两个问题想要解决。第一个是我想要添加一些操作,比如
00:07:03点赞文章和关注作者,但我想要把它们加在标题上方,并且我想要
00:07:08使用客户端组件。目前我只是在使用这个 children 插槽模式,这意味着如果我
00:07:12把我的 post 操作加在下面,它就会跑到评论所在的位置,因为这是我们设置组件的方式,
00:07:17所以我需要一种方法来告诉我的服务器组件把特定的客户端
00:07:22组件放在哪里。然后我们有第二个问题,如果我们有一个关注作者的按钮。目前这个
00:07:27post 页面实际上根本不知道文章作者是谁。我们实际上把所有这些逻辑都卸载到了
00:07:32服务器组件本身中。如果我想要在一个客户端组件中获取作者,
00:07:37我实际上必须为 post 获取 JSON 并以这种方式获取作者,这并不是一个很好的
00:07:42模式,因为我们会双重获取数据。幸运的是,TanStack 其实还有另外两种插槽类型
00:07:46我们可以在复合组件上使用,除了这个 children 插槽,第一个是
00:07:50render props。这本质上就是任何返回 React 元素的函数属性,所以
00:07:56这可以叫任何名字,不一定要叫 renderActions,这里我只是说明
00:07:59我希望服务器组件传递什么数据,也就是 postID 和作者 ID。
00:08:04现在我们只需要在我们的复合组件中,简单地使用这个函数,我们在
00:08:08想要渲染最终组件的地方把它作为属性传递。
00:08:12就我而言,我希望它在卡片头部下方,所以我可以通过 props.render
00:08:16Actions 调用它,我们可以使用可选链,如果它没有被传进来,它也不会崩溃,只是不会
00:08:20渲染,然后我们还可以把我们想要的信息从服务器组件
00:08:24传递给我们的客户端组件。在此之后,我们的复合组件将接受我们刚才创建的 renderActions
00:08:28属性,对于值,我们简单地传递一个函数,该函数将 post
00:08:32ID 和作者 ID 作为参数,服务器将填充这些参数,然后我们简单地渲染我们的
00:08:36postActions 客户端组件,我们可以把数据作为 props 传过去。所以现在我在上面有一个按钮,
00:08:41在那里我可以点赞和复制文章链接,也可以点击关注作者,
00:08:45尽管事实上我从未在这个页面上获取过作者信息,但它却知道作者姓名。
00:08:49我只在服务器组件中获取它,而服务器组件正在把数据传递进
00:08:53客户端组件中。现在你可能会认为这打破了我们之前所说的逻辑,即我们
00:08:57不希望任何服务器组件负责渲染客户端组件,但并没有,因为
00:09:01插槽实际上是不透明的。上面的服务器组件根本不知道这里面是什么,
00:09:06它只知道有些东西会放在这里,并且需要传递这些值,在
00:09:10这种情况下就是 post ID 和作者 ID。这个函数不会在服务器上运行,相反,
00:09:15服务器只是看到它需要传递数据,然后就在我们的客户端,这是函数实际执行
00:09:19以及组件被渲染的时候。同样的事情也适用于
00:09:23我们的第三种插槽类型,即组件属性 (component props),这一个实际上比
00:09:28render props 更简单。我们要做的不是有一个返回我们客户端组件的函数,
00:09:33而是直接把客户端组件本身作为属性传递过去,然后在我们的复合组件
00:09:38定义中,我们声明我们想要接受一个属性,它是一个具有
00:09:42post ID 和作者 ID 属性的 React 组件,然后我们就可以在组件本身内部使用它。你可以认为
00:09:47组件属性就像一个占位符,服务器组件知道那里会有
00:09:51一个组件,它需要一些数据,在我们的例子中是 post ID 和作者 ID,但它并不关心那个
00:09:56组件是什么,只要它接受那些属性即可,所以我把下面的 postActions 组件改成了
00:10:01我制作的另一个叫 fakePostActions 的组件,然后我们保存它,你可以看到
00:10:05它依然会渲染,因为是客户端负责渲染这个组件,
00:10:10只有服务器提供数据。查看文档,似乎没有
00:10:14任何真正的区别,无论你是采用组件属性还是 render 属性,
00:10:18这可能只是归结为个人偏好,我能看到的唯一区别是,也许你想要
00:10:22修改你从服务器获取的数据,在这种情况下,我们可以对
00:10:26post ID 和作者 ID 做任何我们想做的事情,因为这只是一个函数,然后我们可以把它传递给我们的组件,
00:10:31而如果你使用组件属性,你只需要传递组件本身,服务器
00:10:36会负责传递属性。这就是 TanStack 服务器组件的基础,但
00:10:40还有更多值得喜爱的地方。例如,如果你希望页面的大部分内容是服务器渲染的,
00:10:44也许你有一个 header 组件、content 组件和一个 footer 组件,并且你希望它们全部服务器
00:10:49渲染,你不需要把它们全部捆绑到一个 renderServerComponent 函数中,你实际上可以
00:10:53使用 Promise.all,将它们拆分成三个不同的函数,然后简单地将它们作为一个对象
00:10:58从一个单一的 server 函数中返回。但如果其中一个组件加载时间很长呢?那会
00:11:03意味着整个服务器函数都会这样,因此整个页面都会这样。好吧,别担心,这里
00:11:07我们实际上可以做的是,不要 await renderServerComponent 函数,而是
00:11:12返回它创建的 promise,然后在客户端我们可以利用 use hook 和
00:11:16Suspense 边界来加载骨架屏,这样服务器组件就会在准备好时加载。
00:11:21我真的非常喜欢 TanStack 在这里采取的方法,它感觉并不突兀,我不是被迫
00:11:25去采用它,而且我可以在没有任何奇怪的变通方法的情况下采用它。另外,当我真正去使用它时,
00:11:31服务器组件本身实际上只有三个新函数,其余的只是简单的
00:11:36TanStack Start 服务器函数,这是我已经在使用的东西,而且它就像获取数据一样简单。
00:11:41这也意味着它能与像 TanStack Query 这样的工具很好地集成,我肯定会
00:11:45这样做,而且它也使像缓存这样的事情变得更简单。如果愿意的话,你可以字面上
00:11:49直接在你的 CDN 上缓存 GET 请求的响应。我肯定会去探索更多,
00:11:54所以请在下面的评论中告诉我你的想法,以及你是否想看到更多关于它们的视频。
00:11:59嗯,是的,订阅吧,像往常一样,下一期再见。

Key Takeaway

TanStack 服务器组件通过 renderServerComponent 和复合组件模式,允许开发者在保持客户端优先开发习惯的同时,精确地将特定逻辑和组件卸载到服务器运行,从而避免了全量服务器组件架构的复杂性。

Highlights

TanStack 服务器组件提供了一种将服务器渲染逻辑颗粒化集成到现有客户端应用程序中的方案。

renderServerComponent 函数使开发者能够像创建 GET 请求一样在服务器上渲染特定组件。

复合组件 (Composite Components) 允许通过插槽模式将客户端组件注入服务器渲染的树中,无需使用 useClient 指令。

复合组件支持 render props 和 component props 两种插槽类型,用于在不暴露服务器内部细节的情况下传递数据。

开发者可以通过 Promise.all 并行渲染多个服务器组件,并配合客户端的 use hook 与 Suspense 实现加载状态管理。

这种实现方式明确了代码运行环境,且服务器组件的响应可以直接在 CDN 上进行缓存。

Timeline

TanStack 服务器组件的设计理念

  • TanStack 旨在提供一种非侵入式的服务器组件实现,而非强制应用围绕服务器组件构建。
  • 该方法允许像在客户端获取 JSON 一样颗粒化地使用服务器组件。

TanStack 的核心主张是不强制推行默认服务器优先的架构。开发者可以在常规的 TanStack Start 应用中,根据需要按需引入服务器渲染能力,这与完全基于 Next.js 约定的模型形成反面。

renderServerComponent 的实现与使用

  • renderServerComponent 将特定组件转换为可在服务器上渲染的实体。
  • 服务器逻辑通过 server 函数定义,并在路由的 loader 中进行 await 和加载。
  • 此过程类似于发起 GET 请求,代码运行位置在服务器端,具有高度的透明度。

通过 renderServerComponent 函数,开发者可以将 Node.js 环境下的函数(如 os.hostname)集成进组件。所有服务器逻辑清晰地封装在 server 函数内部,不仅降低了复杂性,还保证了代码执行环境的明确界定。

复合组件模式与插槽机制

  • 复合组件通过 createCompositeComponent 辅助函数构建,解决了在服务器组件中嵌套客户端逻辑的冲突。
  • 插槽机制包括 children、render props 和 component props,使得客户端组件能无缝融入服务器渲染的 UI 树中。
  • 客户端组件在复合组件内部渲染时不再需要 useClient 指令。

复合组件允许服务器端组件定义插槽,而不需要决定客户端 UI 的具体实现。render props 和 component props 进一步增强了灵活性,使得服务器仅负责提供数据,而由客户端处理具体 UI 组件的渲染,避免了双重数据获取问题。

性能优化与数据流管理

  • Promise.all 可以并行处理多个服务器组件的渲染,提升整体渲染效率。
  • 利用 use hook 和 Suspense 可以实现服务器组件的异步加载与骨架屏展示。
  • 服务器组件生成的响应结果具备缓存友好性,可直接在 CDN 层进行缓存。

TanStack 这种方法不仅不强制要求特定的组件结构,还兼容现有的生态系统如 TanStack Query。通过将长耗时组件的渲染处理为 promise 并利用 Suspense,页面加载体验得到了显著改善,同时显著简化了服务端数据缓存的实现难度。

Community Posts

No posts yet. Be the first to write about this video!

Write about this video