服务器操作的出现是为了减少客户端代码和简化需要与服务器通信的交互。这是一个优秀的解决方案,可以让开发人员编写更少的代码。然而,在其他框架中实施它存在一些不容忽视的挑战。
在本文中,我们将讨论这些问题以及如何在 Brisa 中找到解决方案。
要了解服务器操作提供的内容,回顾一下过去与服务器的通信方式很有用。您可能习惯于每次与服务器交互时执行以下操作:
这七个动作每次交互都会重复。例如,如果您的页面有 10 个不同的交互,您将重复非常相似的代码 10 次,仅更改请求类型、URL、发送的数据和客户状态等详细信息。
一个熟悉的例子是
一个:
<input onInput={(e) => { // debounce if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fetch("/api/search", { method: "POST", body: JSON.stringify({ query: e.target.value }), }) .then((res) => res.json()) .then((data) => { setState({ data }); }); }, 300); }} />
在服务器中:
app.post("/api/search", async (req, res) => { const { query } = req.body; const data = await search(query); res.json(data); });
增加客户端包大小......以及开发人员的挫败感。
服务器操作将这些操作封装在远程过程调用(RPC)中,它管理客户端-服务器通信,减少客户端上的代码并将逻辑集中在服务器上:
这里的一切都是由 Brisa RPC 为您完成的。
这将是来自服务器组件的代码:
<input onInput={(e) => { // debounce if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fetch("/api/search", { method: "POST", body: JSON.stringify({ query: e.target.value }), }) .then((res) => res.json()) .then((data) => { setState({ data }); }); }, 300); }} />
如您所见,这显着减少了服务器代码,最重要的是,客户端上的代码大小不会随着每次交互而增加。 RPC 客户端代码占用固定的 2 KB,无论您有 10 个还是 1000 个这样的交互。这意味着客户端包大小
增加 0 字节,换句话说,不会增加。
此外,在需要重新渲染的情况下,这是在服务器上完成的,并以 HTML 流的形式返回,使用户比传统方式更早地看到更改,在传统方式中,您必须在客户端上完成此工作服务器响应。
这样:
Brisa Server Actions 与其他框架之间的差异
操作作为 form onSubmit 的一部分,而不是任何事件。
这是一个问题,因为有许多非表单事件也应该从服务器组件处理,而无需添加客户端代码。例如,输入的 onInput 用于执行 自动建议,onScroll 用于加载 无限滚动,onMouseOver 进行悬停等
许多框架也将 HTMX 库视为服务器操作的一个非常不同的替代方案,事实上,它带来了非常好的想法,可以与服务器操作结合起来,只需在 HTML 中添加额外的属性即可发挥更大的潜力。 RPC Client可以考虑,比如我们之前见过的debounceInput。还有其他 HTMX 想法,例如在发出请求时显示旋转器的指示器,或者能够处理 RPC 客户端中的错误。
当服务器操作在 React 中引入时,出现了新的范式转变,许多开发人员在使用它们时必须更改心理芯片。
我们希望使其尽可能熟悉Web平台,这样,您就可以从服务器捕获序列化事件并使用其属性。唯一有点不同的事件是已经传输了 FormData 并具有 e.formData 属性的 onSubmit,不过,其余的事件属性都是可交互的。这是一个示例重置表单:
<input onInput={(e) => { // debounce if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fetch("/api/search", { method: "POST", body: JSON.stringify({ query: e.target.value }), }) .then((res) => res.json()) .then((data) => { setState({ data }); }); }, 300); }} />
在此示例中,根本没有客户端代码,在服务器操作期间,您可以使用 CSS 使用指示器禁用提交按钮,以便表单无法提交两次,并且在在服务器上执行操作后的同一时间,使用 e.formData 访问表单数据,然后使用事件的相同 API 重置表单。
在精神上,这与使用网络平台非常相似。唯一的区别是所有服务器组件的所有事件都是服务器操作。
这样,就实现了真正的关注点分离,没有必要将“用户服务器”或“使用客户端”放入您的组件不再。
请记住一切都只在服务器上运行。唯一的例外是在客户端上运行的src/web-components文件夹,并且事件正常。
在 Brisa 中,服务器操作在服务器组件之间传播,就好像它们是 DOM 事件一样。也就是说,从一个Server Action中,你可以调用一个Server Component的prop的事件,然后执行父Server Component的Server Action,等等
<input onInput={(e) => { // debounce if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fetch("/api/search", { method: "POST", body: JSON.stringify({ query: e.target.value }), }) .then((res) => res.json()) .then((data) => { setState({ data }); }); }, 300); }} />
在这种情况下,onAfterMyAction 事件在父组件上执行,并且可以在服务器上执行操作。这对于在服务器上执行影响多个服务器组件的操作非常有用。
特别是在过去的几周里,在 X(以前的 Twitter)上进行了多次讨论之后,Web 组件变得有点不受欢迎。然而,作为HTML的一部分,它是与服务器操作交互的最佳方式,原因如下:
在 Web Components 中使用属性需要序列化,就像不使用 Web Components 将数据从服务器传输到客户端一样,因此,使用两者,无需管理额外的序列化。
注意:如果您有兴趣,我会在另一篇文章中解释流式 HTML 并使用比较算法对其进行处理。
在 Brisa 中,我们添加了一个新概念,为服务器操作提供更多功能,这个概念称为 “操作信号”。 “行动信号”的想法是,您有2 个商店,一个位于服务器,一个位于客户端。
为什么是2家店?
默认服务器存储 仅在请求级别存在。并且您可以共享对客户端不可见的数据。例如,您可以让中间件设置用户并有权访问任何服务器组件中的敏感用户数据。通过生活在请求级别,不同请求之间不可能发生冲突,因为每个请求都有其自己的存储并且不存储在任何数据库中,当请求完成,默认消亡。
另一方面,在客户端商店中,它是一个商店,每个属性在消费时都是一个信号,也就是说,如果它已更新,正在侦听该信号的 Web 组件会做出反应。
但是,“操作信号”的新概念是我们可以将服务器存储的生命周期延长到请求之外。为此,需要使用以下代码:
<input onInput={(e) => { // debounce if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fetch("/api/search", { method: "POST", body: JSON.stringify({ query: e.target.value }), }) .then((res) => res.json()) .then((data) => { setState({ data }); }); }, 300); }} />
此transferToClient方法,将服务器数据共享到客户端存储并转换为信号。通过这种方式,很多时候不需要从服务器进行任何重新渲染,您可以简单地通过服务器操作对正在侦听该信号的 Web 组件的信号做出反应。
这次商店转移使服务器商店的生命现在:
渲染初始服务器组件→客户端→服务器操作→客户端→服务器操作...
因此它从仅在请求级别生存到永久生存,兼容页面之间的导航。
示例:
app.post("/api/search", async (req, res) => { const { query } = req.body; const data = await search(query); res.json(data); });
在此示例中,我们延长了错误存储属性的寿命,不是在客户端上使用,而是在服务器操作中重用,然后最终在服务器操作的重新渲染中重用。在这种情况下,作为非敏感数据,没有必要对其进行加密。此示例代码全部发生在服务器上,甚至重新渲染,用户将在服务器上渲染后看到错误,其中服务器 RPC 将在流中发送 HTML 块,客户端 RPC 将对其进行处理以进行比较并显示向用户反馈错误。
如果在服务器操作中使用了渲染级别存在的某个变量,则在安全级别,许多框架(如 Next.js 14)所做的就是加密此数据以创建在以下位置使用的数据的快照渲染的时间。这或多或少没问题,但是始终加密数据会产生相关的计算成本并且它并不总是敏感数据。
在 Brisa 中,为了解决这个问题,有不同的请求,在初始渲染中它有一个值,并且在服务器操作中您可以捕获它在此请求中具有的值。
<input debounceInput={300} onInput={async (e) => { // All this code only runs on the server const data = await search(e.target.value); store.set("query", data); store.transferToClient(["query"]); }} />
这在某些情况下很有用,但并不总是有用,例如,如果您执行 Math.random,初始渲染和服务器操作执行之间肯定会有所不同。
<input onInput={(e) => { // debounce if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { fetch("/api/search", { method: "POST", body: JSON.stringify({ query: e.target.value }), }) .then((res) => res.json()) .then((data) => { setState({ data }); }); }, 300); }} />
这就是为什么我们创建了“行动信号”概念,以将数据从服务器存储传输到客户端存储,并且开发者可以随意决定是否加密它或不。
有时,您可能希望传输初始渲染中已存在的数据,而不是从服务器操作查询数据库,即使它需要关联的加密。为此,您只需使用:
app.post("/api/search", async (req, res) => { const { query } = req.body; const data = await search(query); res.json(data); });
当你这样做时:
<input debounceInput={300} onInput={async (e) => { // All this code only runs on the server const data = await search(e.target.value); store.set("query", data); store.transferToClient(["query"]); }} />
Web 组件(客户端)内部始终会加密,但在服务器上始终会解密。
注意:Brisa 使用 aes-256-cbc 进行加密,这是 OpenSSL 推荐的用于安全加密信息的加密算法组合。加密密钥是在项目构建过程中生成的。
在 Brisa 中,虽然我们喜欢支持轻松编写 Web 组件,但目标是能够在没有客户端代码的情况下制作 SPA,并且仅在纯客户端交互或必须触及 Web API 时才使用 Web 组件。这就是为什么服务器操作如此重要,因为它们允许与服务器交互而无需编写客户端代码。
我们鼓励您尝试 Brisa,您只需在终端中运行此命令:bun create brisa,或者尝试一些示例来看看它是如何工作的。
以上是服务器操作已修复的详细内容。更多信息请关注PHP中文网其他相关文章!