React Server Component 在 Shopify 中的最佳实践

2022-03-07 14:58:01 浏览数 (1)

Shopify 是国外的一个允许客户自由搭建商城的 nocode 产品,工程师 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 启动模板来试几个例子。

订阅注册

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

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

代码语言:javascript复制
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组件里:

代码语言:javascript复制
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 组件来使用这个客户端组件:

代码语言:javascript复制
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 代码示例 中查看这个时事通讯注册组件。

产品常见问题组件

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

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

代码语言:javascript复制
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 页面来避免这种情况:

代码语言:javascript复制
import ProductFAQs from '../../components/ProductFAQs';

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

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

代码语言:javascript复制
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:

代码语言:javascript复制
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

代码语言:javascript复制
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 代码

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

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

0 人点赞