在 Shopify 探索 React Server Component 的最佳实践

简介: Shopify 是国外的一个允许客户自由搭建商城的 no code 产品,工程师 Cathryn Griffiths 分享了他在 Shopify 中实用 React Server Component 的最佳实践。

Shopify 是国外的一个允许客户自由搭建商城的 no code 产品,工程师 Cathryn Griffiths 分享了他在 Shopify 中实用 React Server Component 的最佳实践。

Hydrogen 是基于 React 的框架用来创建自定义店面的框架,他们试用 RSC(React Server Component)有两个理由:

  1. 再见了,臃肿的 bundle 体积,你好,更棒的购物体验!
  2. 技术人的一种自私情结:这玩意一定很有趣!

这是一件很有挑战性的事。RSC 是一种范式转变,一开始他们遇到的问题是构建的客户端组件太多,服务器组件太少。经过数月的反复尝试和重构才找到较好的方案。

这篇文章将着重讨论工程师在构建 Hydrogen 时候发现的 RSC 最佳实践,不光是对个人的,也是对团队的。希望能让读者们更加理解如何在 RSC 应用中编写组件,减少你的无效时间。


优先写共享组件


当你需要在 RSC 应用程序中从头构建组件时,请从共享组件开始。共享组件可以同时在服务器和客户端上下文中执行,而不会出现任何问题。它们是客户端和服务器组件之间的天然中间地带,是个不错的起点。

从中间地带开始,可以帮助你更好的思考,引导你构建正确类型的组件。你必须问自己:“这段代码只能在客户机上运行吗?”,类似地,“这段代码应该在客户机上执行吗?”下一节列出了一些您应该问的问题。

不要总是默认构建客户端组件。虽然方便,但最后应用程序会太臃肿,很多组建更适合在服务端运行。


在少数情况下选择客户端组件


RSC 应用程序中的大多数组件应该是服务器组件,因此在确定是否需要客户端组件时,需要仔细分析用例。

通常只有客户端特定的逻辑部分需要被提取到客户端组件中:

  • 整合客户端交互性
  • 用了 useStateuseReducer
  • 用了生命周期渲染逻辑(比如 useEffect
  • 用了不支持 RSC 的第三方库
  • 用了服务端不支持的浏览器 APIs

重要说明:不要只是盲目将整个共享组件转换为客户端组件。相反,有意地提取需要的特定功能。这有助于保持您的客户端组件和 bundle 尺寸尽可能的小。文章末尾会有一些示例。


尽可能以服务端组件为主


如果组件不包含任何客户端组件用例,那么它应该被改为服务器组件(如果它符合以下条件之一):

  • 该组件包含不应该在客户端上暴露的代码,如专用业务逻辑和密钥。
  • 客户端组件中不会使用该组件。(RSC 的限制,客户端组件中不能直接导入服务端组件)
  • 代码从不在客户端上执行(据你所知)。
  • 代码需要访问文件系统或数据库(客户端上不可用)。
  • 代码需要从 StoreFront API 获取数据(在 Hydrogen 中特定的情况)。

如果组件需要在客户端组件中使用,可以先深入研究用例和实现。很可能你可以将组件实例作为 children props 传递给客户端组件,而不是让客户端组件直接导入并实用它。这样就不需要把组件转换为客户端组件了。


探索一些例子


有很多东西需要记住,我们可以用 Hydrogen 启动模板[1]来试几个例子。


订阅注册


第一个示例是一个组件,它允许买家注册订阅我的在线商店的时事通讯。它出现在每个页面的页脚,看起来像这样:

我们从一个名为 NewsletterSignup.jsx的共享组件开始:

export default function NewsletterSignup() {
  return (
    <div>
      <p>
        Sign up for our newsletter to never miss out on latest news and product
        drops!
      </p>
      <label for="emailInput">Email</label>
      <input type="text" id="emailInput" name="email" placeholder="Email" />
      <button
        onClick={() => {
          /* TODO */
        }}
      >
        Sign me up
      </button>
    </div>
  );
}

在这个组件中,我们有两个客户端交互部分(输入字段和提交按钮),这说明这个当前编写的组件不能是共享组件。

我们别将其完全转换为客户端组件,而是将客户端功能提取到一个单独的 NewsletterSignupForm.client.jsx组件里:

export default function NewsletterSignupForm() {
  return (
    <>
      <label for="emailInput">Email</label>
      <input type="text" id="emailInput" name="email" placeholder="Email" />
      <button
        onClick={() => {
          /* TODO */
        }}
      >
        Sign me up
      </button>
    </>
  );
}

然后更新 NewsletterSignup 组件来使用这个客户端组件:

import NewsletterSignupForm from './NewsletterSignupForm.client';
export default function NewsletterSignup() {
  return (
    <div>
      <p>
        Sign up for our newsletter to never miss out on latest news and product
        drops!
      </p>
      <NewsletterSignupForm />
    </div>
  );
}

我们很容易到此为止,并将 NewsletterSignup 组件保持为一个共享组件。然而我知道这个组件只在我的在线商店的页脚中使用,而我的页脚组件是一个服务端组件。所以它不需要是一个共享组件,也不需要成为客户端 bundle 的一部分,简单地将其重命名为 NewsletterSignup.server.jsx来安全地将其更改为服务端组件。

搞定,你可以在最终的 Stackblitz 代码示例[2] 中查看这个时事通讯注册组件。


产品常见问题组件


在下一个示例中,我们将产品常见问题部分添加到产品页面。这里的内容是静态的,对我的在线商店中的每个产品都是一样的。来自买家的互动可以展开或收起内容。它看起来是这样的:

让我们从一个共享的ProductFAQs.jsx开始。jsx 组件:

export default function ProductFAQs() {
  return (
    <ul>
      <li>
        <span>Where was this board made?</span>
        <p>
          All our boards are designed in Canada by our Hydrogen design team.
        </p>
        <p>Materials are sourced from local manufacturers.</p>
        <p>
          Assembly is done by our skilled team on site in our brick and mortar
          shop.
        </p>
      </li>
      <li>
        <span>What if I don't like it?</span>
        <p>
          The Hydrogen team stands by their products. We strive to delivery high
          quality boards that will last a lifetime and, importantly, make you
          happy.
        </p>
        <p>
          That said, if you don't like it, you can return it to us (free of
          cost!) and we'll reimburse you the money. Contact us directly for more
          details.
        </p>
      </li>
    </ul>
  );
}

接下来,我们将把它添加到产品页面。ProductDetails.client 组件用于展示此页面的主要内容,因此很容易把ProductFAQs转换为客户端组件,这样 ProductDetails 组件可以直接导入使用它。但是,我们可以通过将 ProductFAQs 传递给 product/[handle].server.jsx 页面来避免这种情况:

import ProductFAQs from '../../components/ProductFAQs';
export default function Product({ country = { isoCode: 'US' } }) {
  // ...
  return (
    <Layout>
      <ProductDetails product={data.product}>
        <ProductFAQs />
      </ProductDetails>
    </Layout>
  );
}

然后更新 ProductDetails组件来使用 children:

export default function ProductDetails({ product, children }) {
  // ...
  return (
    <>
      <Seo product={product} />
      <Product product={product} initialVariantId={initialVariant.id}>
        ...
      </Product>
      {children}
    </>
  );
}

接下来,我们想要将客户端交互部分添加到 ProductFAQs 组件。同样,我们很容易直接将 ProductFAQ 组件从共享组件转换为客户端组件,但没必要。这些交互仅用于展开和收起 FAQ 内容,而内容本身是硬编码的,不需要成为客户端 bundle 的一部分。我们要做的是将客户端交互提取到一个专门的客户端组件Accordion.client.jsx:

import { useState } from 'react';
export default function Accordion({ heading, children }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <div
        onClick={() => {
          setOpen(!open);
        }}
      >
        <span>{heading}</span>
        <span>{open ? '-' : '+'}</span>
      </div>
      {open && children}
    </div>
  );
}

更新ProductFAQs组件来使用Accordion

import Accordion from './Accordion.client';
export default function ProductFAQs() {
  return (
    <ul>
      <li>
        <Accordion heading="Where was this board made?">
          <>
            <p>
              All our boards are designed in Canada by our Hydrogen design team.
            </p>
            <p>Materials are sourced from local manufacturers.</p>
            <p>
              Assembly is done by our skilled team on site in our brick and
              mortar shop.
            </p>
          </>
        </Accordion>
      </li>
      <li>
        <Accordion heading="What if I don't like it?">
          <>
            <p>
              The Hydrogen team stands by their products. We strive to delivery
              high quality boards that will last a lifetime and, importantly,
              make you happy.
            </p>
            <p>
              That said, if you don't like it, you can return it to us (free of
              cost!) and we'll reimburse you the money. Contact us directly for
              more details.
            </p>
          </>
        </Accordion>
      </li>
    </ul>
  );
}

此时,不再有理由让 ProductFAQs 组件保持为共享组件了。所有的客户端交互都已经被提取出来,并且,类似于NewsletterSignup组件,我知道这个组件永远不会被客户端组件使用。现在剩下的就是:

  • 重命名 ProductFAQs.jsx 文件为 ProductFAQs.server.jsx
  • 更新 product/[handle].server.jsx 中的 import 声明
  • 通过 Tailwind 添加一些漂亮的样式。

你可以在 Stackblitz 中查看 Product FAQ 代码[3]

React Server Components 是一种范式转变,为 RSC 应用程序编写组件可能需要一些时间来适应。当你在构建时,请记住以下几点:

  • 从共享组件开始。
  • 在特定情况下,将功能提取到客户端组件中。
  • 如果代码永远不需要或永远不应该在客户机上执行,则改写为服务端组件。

享受 coding 吧!

Cathryn 是 Shopify Checkout 团队的前端开发人员,也是 Hydrogen 的创始成员。她在加拿大蒙特利尔远程工作。当不写代码的时候,她通常会和她的狗玩、做手工或阅读。

参考:https://shopify.engineering/react-server-components-best-practices-hydrogen

相关文章
|
1月前
|
前端开发
react-router中的render、children、component
react-router中的render、children、component
40 1
|
3月前
|
前端开发
React Component和Purecomponent区别
React Component和Purecomponent区别
19 0
|
8月前
|
资源调度 前端开发 数据库
前端项目实战拾柒-react-admin+postgrest+material ui最佳实践展示1
前端项目实战拾柒-react-admin+postgrest+material ui最佳实践展示1
71 0
|
4月前
|
弹性计算 前端开发 JavaScript
前端新趋势?有了Web Component,还用纠结Vue或React?
Web Component 的概念最早在 2011 年被 Google 提出,并在 2018 年 V1 版本开始被主流浏览器所支持(除了 IE)。
|
8月前
|
前端开发
前端项目实战玖拾贰react-admin+material ui-踩坑-List的用法之component用法
前端项目实战玖拾贰react-admin+material ui-踩坑-List的用法之component用法
35 0
|
8月前
|
资源调度 前端开发 数据库
react-admin+postgrest+material ui最佳实践展示
react-admin+postgrest+material ui最佳实践展示
34 0
|
9月前
|
存储 缓存 前端开发
构建 React Monorepo 的最佳实践
构建 React Monorepo 的最佳实践
216 0
|
9月前
|
JSON 前端开发 JavaScript
关于做好React项目,你需要知道的几点最佳实践
关于做好React项目,你需要知道的几点最佳实践
140 0
|
9月前
|
缓存 前端开发 JavaScript
React 数据请求最佳实践
React 数据请求最佳实践
342 0
|
3月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
105 0