推薦一個(gè) React Server Component 在大公司的落地案例
Shopify 是國外的一個(gè)允許客戶自由搭建商城的 no code 產(chǎn)品,工程師 Cathryn Griffiths 分享了他在 Shopify 中實(shí)用 React Server Component 的最佳實(shí)踐。
Hydrogen 是基于 React 的框架用來創(chuàng)建自定義店面的框架,他們試用 RSC(React Server Component)有兩個(gè)理由:
再見了,臃腫的 bundle 體積,你好,更棒的購物體驗(yàn)! 技術(shù)人的一種自私情結(jié):這玩意一定很有趣!
這是一件很有挑戰(zhàn)性的事。RSC 是一種范式轉(zhuǎn)變,一開始他們遇到的問題是構(gòu)建的客戶端組件太多,服務(wù)器組件太少。經(jīng)過數(shù)月的反復(fù)嘗試和重構(gòu)才找到較好的方案。
這篇文章將著重討論工程師在構(gòu)建 Hydrogen 時(shí)候發(fā)現(xiàn)的 RSC 最佳實(shí)踐,不光是對個(gè)人的,也是對團(tuán)隊(duì)的。希望能讓讀者們更加理解如何在 RSC 應(yīng)用中編寫組件,減少你的無效時(shí)間。
優(yōu)先寫共享組件
當(dāng)你需要在 RSC 應(yīng)用程序中從頭構(gòu)建組件時(shí),請從共享組件開始。共享組件可以同時(shí)在服務(wù)器和客戶端上下文中執(zhí)行,而不會出現(xiàn)任何問題。它們是客戶端和服務(wù)器組件之間的天然中間地帶,是個(gè)不錯(cuò)的起點(diǎn)。
從中間地帶開始,可以幫助你更好的思考,引導(dǎo)你構(gòu)建正確類型的組件。你必須問自己:“這段代碼只能在客戶機(jī)上運(yùn)行嗎?”,類似地,“這段代碼應(yīng)該在客戶機(jī)上執(zhí)行嗎?”下一節(jié)列出了一些您應(yīng)該問的問題。
不要總是默認(rèn)構(gòu)建客戶端組件。雖然方便,但最后應(yīng)用程序會太臃腫,很多組建更適合在服務(wù)端運(yùn)行。
在少數(shù)情況下選擇客戶端組件
RSC 應(yīng)用程序中的大多數(shù)組件應(yīng)該是服務(wù)器組件,因此在確定是否需要客戶端組件時(shí),需要仔細(xì)分析用例。
通常只有客戶端特定的邏輯部分需要被提取到客戶端組件中:
整合客戶端交互性 用了 useState或useReducer用了生命周期渲染邏輯(比如 useEffect)用了不支持 RSC 的第三方庫 用了服務(wù)端不支持的瀏覽器 APIs
重要說明:不要只是盲目將整個(gè)共享組件轉(zhuǎn)換為客戶端組件。相反,有意地提取需要的特定功能。這有助于保持您的客戶端組件和 bundle 尺寸盡可能的小。文章末尾會有一些示例。
盡可能以服務(wù)端組件為主
如果組件不包含任何客戶端組件用例,那么它應(yīng)該被改為服務(wù)器組件(如果它符合以下條件之一):
該組件包含不應(yīng)該在客戶端上暴露的代碼,如專用業(yè)務(wù)邏輯和密鑰。
客戶端組件中不會使用該組件。(RSC 的限制,客戶端組件中不能直接導(dǎo)入服務(wù)端組件)
代碼從不在客戶端上執(zhí)行(據(jù)你所知)。
代碼需要訪問文件系統(tǒng)或數(shù)據(jù)庫(客戶端上不可用)。
代碼需要從 StoreFront API 獲取數(shù)據(jù)(在 Hydrogen 中特定的情況)。
如果組件需要在客戶端組件中使用,可以先深入研究用例和實(shí)現(xiàn)。很可能你可以將組件實(shí)例作為 children props 傳遞給客戶端組件,而不是讓客戶端組件直接導(dǎo)入并實(shí)用它。這樣就不需要把組件轉(zhuǎn)換為客戶端組件了。
探索一些例子
有很多東西需要記住,我們可以用 Hydrogen 啟動模板[1]來試幾個(gè)例子。
訂閱注冊
第一個(gè)示例是一個(gè)組件,它允許買家注冊訂閱我的在線商店的時(shí)事通訊。它出現(xiàn)在每個(gè)頁面的頁腳,看起來像這樣:

我們從一個(gè)名為 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">Emaillabel>
??????<input?type="text"?id="emailInput"?name="email"?placeholder="Email"?/>
??????<button
????????onClick={()?=>?{
??????????/*?TODO?*/
????????}}
??????>
????????Sign?me?up
??????button>
????div>
??);
}
在這個(gè)組件中,我們有兩個(gè)客戶端交互部分(輸入字段和提交按鈕),這說明這個(gè)當(dāng)前編寫的組件不能是共享組件。
我們別將其完全轉(zhuǎn)換為客戶端組件,而是將客戶端功能提取到一個(gè)單獨(dú)的 NewsletterSignupForm.client.jsx組件里:
export?default?function?NewsletterSignupForm()?{
??return?(
????<>
??????<label?for="emailInput">Emaillabel>
??????<input?type="text"?id="emailInput"?name="email"?placeholder="Email"?/>
??????<button
????????onClick={()?=>?{
??????????/*?TODO?*/
????????}}
??????>
????????Sign?me?up
??????button>
????>
??);
}
然后更新 NewsletterSignup 組件來使用這個(gè)客戶端組件:
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 組件保持為一個(gè)共享組件。然而我知道這個(gè)組件只在我的在線商店的頁腳中使用,而我的頁腳組件是一個(gè)服務(wù)端組件。所以它不需要是一個(gè)共享組件,也不需要成為客戶端 bundle 的一部分,簡單地將其重命名為 NewsletterSignup.server.jsx來安全地將其更改為服務(wù)端組件。
搞定,你可以在最終的 Stackblitz 代碼示例[2] 中查看這個(gè)時(shí)事通訊注冊組件。
產(chǎn)品常見問題組件
在下一個(gè)示例中,我們將產(chǎn)品常見問題部分添加到產(chǎn)品頁面。這里的內(nèi)容是靜態(tài)的,對我的在線商店中的每個(gè)產(chǎn)品都是一樣的。來自買家的互動可以展開或收起內(nèi)容。它看起來是這樣的:

讓我們從一個(gè)共享的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>
??);
}
接下來,我們將把它添加到產(chǎn)品頁面。ProductDetails.client 組件用于展示此頁面的主要內(nèi)容,因此很容易把ProductFAQs轉(zhuǎn)換為客戶端組件,這樣 ProductDetails 組件可以直接導(dǎo)入使用它。但是,我們可以通過將 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 組件從共享組件轉(zhuǎn)換為客戶端組件,但沒必要。這些交互僅用于展開和收起 FAQ 內(nèi)容,而內(nèi)容本身是硬編碼的,不需要成為客戶端 bundle 的一部分。我們要做的是將客戶端交互提取到一個(gè)專門的客戶端組件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>
????l>
??);
}
此時(shí),不再有理由讓 ProductFAQs 組件保持為共享組件了。所有的客戶端交互都已經(jīng)被提取出來,并且,類似于NewsletterSignup組件,我知道這個(gè)組件永遠(yuǎn)不會被客戶端組件使用?,F(xiàn)在剩下的就是:
重命名
ProductFAQs.jsx文件為ProductFAQs.server.jsx更新
product/[handle].server.jsx中的 import 聲明通過 Tailwind 添加一些漂亮的樣式。
你可以在 Stackblitz 中查看 Product FAQ 代碼[3]
React Server Components 是一種范式轉(zhuǎn)變,為 RSC 應(yīng)用程序編寫組件可能需要一些時(shí)間來適應(yīng)。當(dāng)你在構(gòu)建時(shí),請記住以下幾點(diǎn):
從共享組件開始。
在特定情況下,將功能提取到客戶端組件中。
如果代碼永遠(yuǎn)不需要或永遠(yuǎn)不應(yīng)該在客戶機(jī)上執(zhí)行,則改寫為服務(wù)端組件。
享受 coding 吧!
Cathryn 是 Shopify Checkout 團(tuán)隊(duì)的前端開發(fā)人員,也是 Hydrogen 的創(chuàng)始成員。她在加拿大蒙特利爾遠(yuǎn)程工作。當(dāng)不寫代碼的時(shí)候,她通常會和她的狗玩、做手工或閱讀。
參考:https://shopify.engineering/react-server-components-best-practices-hydrogen
關(guān)注公眾號后,在首頁:
回復(fù)指南,高級前端、算法學(xué)習(xí)路線,是我自己一路走來的實(shí)踐。 回復(fù)簡歷,大廠簡歷編寫指南,是我看了上百份簡歷后總結(jié)的心血。 參考資料
關(guān)注公眾號后,在首頁:
指南,高級前端、算法學(xué)習(xí)路線,是我自己一路走來的實(shí)踐。簡歷,大廠簡歷編寫指南,是我看了上百份簡歷后總結(jié)的心血。Hydrogen 啟動模板: https://hydrogen.new/
[2]Stackblitz 代碼示例: https://stackblitz.com/edit/shopify-hydrogen-9emcac?file=src%2Fcomponents%2FNewsletterSignup.server.jsx
[3]Product FAQ 代碼: https://stackblitz.com/edit/shopify-hydrogen-9emcac?file=src%2Fcomponents%2FProductFAQs.server.jsx&title=Hydrogen
