初窺 Shopify Hydrogen 框架以及 React Server Components

C.T. Lin
9 min readJan 5, 2022

--

Hydrogen 是 Shopify 打造的一個基於 React 的 Framework,可以用來製作客製化的店面網站,其最大的賣點是支援 React Server Components(RSC),這應該算是全球走在最前沿的 RSC 大型實驗。

Hydrogen 依賴在一些蠻知名的套件/技術之上,包括:Vite、GraphQL、React Router。它也提供一些 Components、Hooks、Utilities 來幫助店面網站開發。

Hydrogen 架構

從這張圖可以看到,Hydrogen 做的網站可以跑在 Node.js 上,也可以跑在 Worker 上(Shopify 的 Oxygen 平台或是 Cloudflare Worker)。

開發的時候使用 Vite Development Server,會有處理 cache 的 layer,再來就是進到 entry-server,主要的 App component,再看是要 render 哪個 page。

使用 create-hydrogen-app 建出來的 folder,裡面會有這樣的結構:

└── src
├── components
└── Button.client.jsx
└── Cart.client.jsx
└── CartIcon.client.jsx
└── ...
├── pages
└── collections
└── [handle].server.jsx
└── pages
└── [handle].server.jsx
└── products
└── [handle].server.jsx
└── index.server.jsx
└── sitemap.xml.server.jsx
├── App.server.jsx
├── entry-client.jsx
├── entry-server.jsx
├── index.css

其中包括 file-based routing system、主要的 App component — App.server.jsx、以及 entry-client.jsxentry-server.jsx 兩個進入點。

Server Components

學習 React Server Components(RSC),是理解 Hydrogen 的其中一個環節,我之前有寫了一篇「React 新概念 — Server Components」的介紹,有興趣可以看看。

簡單來說,component 必須被分成:

  • .server.jsx — 在 server 上 render,不能使用 client-only feature
  • .client.jsx — 在 client 上 render,不能使用 server-only feature
  • .jsx — 可以同時在 client 跟 server 上 render

React core team 之前只做了 Webpack 的 demo,Hydrogen 能支援 RSC,是依靠 Shopify 自行開發了 Vite 上的實作

所有 route 的部分,都是使用 RSC,例如:

└── src
├── pages
└── products
└── [handle].server.jsx
└── index.server.jsx

這樣子的檔案可以各自對應到:

  • pages/index.server.jsx — localhost:3000/
  • pages/products/[handle].server.jsx— localhost:3000/products/<handle>

這些 page server component 會接收 requestresponse 作為 props,可以針對它們作出處理:

function MyPage({ request, response }) {
if (request.headers.get('my-custom-header') === SOME_VALUE) {
// Do something based on a header
}
response.headers.set('custom-header', 'value'); // ...
}

在 client 用 setServerState 設定的 server state 也會傳遞到 page server components 當作 props:

function MyPage({ custom, state, here }) {
// Use custom server state
}

server state 是一種 client component 跟 server component 溝通 state 的機制。例如,這個 MyPage server component 接收一個 selectedProductId prop:

export default function MyPage({ selectedProductId }) {
const { data } = useShopQuery({
query: QUERY,
variables: { productId: selectedProductId },
});
const { product } = data;
return (
<>
<div>Selected product is {product.title}</div>
<ProductSelector selectedProductId={selectedProductId} />
</>
);
}

在 client component 可以用 useServerState hook 來取得 setServerState ,並在 onClick 的時候用 setServerState 來設定 MyPageselectedProductId來重新 render server component:

import { useServerState } from '@shopify/hydrogen/client';export default function ProductSelector({ selectedProductId }) {
const { setServerState } = useServerState();
return (
<div>
<button
onClick={() => {
setServerState('selectedProductId', 123);
}}
>
Select Shoes
</button>
<button
onClick={() => {
setServerState('selectedProductId', 456);
}}
>
Select Dresses
</button>
</div>
);
}

流程會是這個樣子:

好用的 useShopQuery

Shopify 本來就有提供相當好用的 GraphQL API,因此 Hydrogen 也提供了一個 useShopQuery 可以輕鬆地做查詢:

import { useShopQuery } from '@shopify/hydrogen';
import gql from 'graphql-tag';
export default function Blog() {
const { data } = useShopQuery({
query: QUERY,
variables: {
handle: 'frontpage',
},
});
return <h1>{data.blog.articles.edges[0].node.title}</h1>;
}
const QUERY = gql`
query blogContent($handle: String!) {
blog: blogByHandle(handle: $handle) {
articles(first: 1) {
edges {
node {
id
title
}
}
}
}
}
`;

Caching

Hydrogen 有提供兩種不同的 cache 機制:

在使用 useShopQuery 的時候可以去設定請求的 Cache-Control:

const { data } = useShopQuery({
query: QUERY,
cache: {
// Cache the data for one second.
maxAge: 1,
// Serve stale data for up to nine seconds while getting a fresh response in the background.
staleWhileRevalidate: 9,
},
});

如果是沒有動態資料的頁面,可以進一步的使用全頁 cache:

export default function MyProducts({ response }) {
response.cache({
// Cache the page for one hour.
maxAge: 60 * 60,
// Serve the stale page for up to 23 hours while getting a fresh response in the background.
staleWhileRevalidate: 23 * 60 * 60,
});
}

介紹大概就到這裡了,未來想要做類似架構可以參考看看。

--

--

C.T. Lin
C.T. Lin

Written by C.T. Lin

Architect @ Dcard. Author of Electron React Boilerplate and Bottender. JavaScript Developer.