如何在 React 18中 利用Suspense 實(shí)現(xiàn) 服務(wù)端渲染(SSR)
如何在服務(wù)端使用React18
https://github.com/reactwg/react-18/discussions/22
服務(wù)器端渲染(在本文中縮寫為“SSR”)讓您可以從服務(wù)器上的 React 組件生成 HTML,并將該 HTML 發(fā)送給您的用戶。SSR 允許您的用戶在您的 JavaScript 包加載和運(yùn)行之前查看頁(yè)面的內(nèi)容。
React 中的 SSR 總是發(fā)生在幾個(gè)步驟中:
在服務(wù)器端,獲取整個(gè)應(yīng)用程序的數(shù)據(jù)。
在服務(wù)器端,將整個(gè)應(yīng)用程序呈現(xiàn)為 HTML 字符串并將其發(fā)送到客戶端。
在客戶端,加載整個(gè)應(yīng)用程序的JavaScript 代碼。
在客戶端,將JavaScript 邏輯連接到整個(gè)應(yīng)用程序的服務(wù)器生成的HTML(這就是“hydration”)。
關(guān)鍵部分是,在下一步開始之前,整個(gè)應(yīng)用程序的每個(gè)步驟都必須立即完成。如果其中一個(gè)環(huán)節(jié)比其他部分慢,將會(huì)影響整體的加載時(shí)間。
React18中,您可以使用?

此插圖使用綠色表示頁(yè)面的這些部分是交互式的。換句話說(shuō),它們所有的 JavaScript 事件處理程序都已附加,單擊按鈕可以更新?tīng)顟B(tài),等等。
但是,頁(yè)面在 JavaScript 代碼完全加載之前無(wú)法進(jìn)行交互。這包括 React 本身和您的應(yīng)用程序代碼。對(duì)于非React的應(yīng)用程序,大部分加載時(shí)間將用于下載您的應(yīng)用程序代碼。
如果您不使用 SSR,則用戶在 JavaScript 加載時(shí)只會(huì)看到一個(gè)空白頁(yè)面:


當(dāng) React 和你的應(yīng)用程序代碼都加載時(shí),你想讓這個(gè) HTML 交互。你告訴 React:“這是App在服務(wù)器上生成這個(gè) HTML的組件。將事件處理程序附加到該 HTML!” React 將在內(nèi)存中渲染你的組件樹,但它不會(huì)為它生成 DOM 節(jié)點(diǎn),而是將所有邏輯附加到現(xiàn)有的 HTML。
渲染組件和附加事件處理程序的過(guò)程稱為“水化”。這就像用交互性和事件處理程序的“水”去澆灌“干涸”的 HTML。(或者至少,這就是我對(duì)自己解釋這個(gè)術(shù)語(yǔ)的方式。)
水合之后,它是“像往常一樣反應(yīng)”:你的組件可以設(shè)置狀態(tài),響應(yīng)點(diǎn)擊等等:

你可以看到 SSR 是一種“魔術(shù)”。它不會(huì)使您的應(yīng)用程序完全交互更快。相反,它可以讓您更快地顯示應(yīng)用程序的非交互式版本,以便用戶可以在等待 JS 加載時(shí)查看靜態(tài)內(nèi)容。然而,這個(gè)技巧對(duì)網(wǎng)絡(luò)連接不佳的人產(chǎn)生了巨大的影響,并提高了整體感知性能。由于其更容易的索引和更快的速度,它還可以幫助您進(jìn)行搜索引擎排名。
注意:不要將 SSR 與服務(wù)器組件混淆。服務(wù)器組件是一個(gè)更具實(shí)驗(yàn)性的功能,仍在研究中,可能不會(huì)成為最初的 React 18 版本的一部分。您可以復(fù)制以下連接了解服務(wù)器組件。服務(wù)器組件是對(duì) SSR 的補(bǔ)充,并將成為推薦的數(shù)據(jù)獲取方法的一部分,但本文與它們無(wú)關(guān)。
React服務(wù)器組件
https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html
上述方法有效,但在很多方面表現(xiàn)不佳。
1. 您必須獲取所有內(nèi)容,然后才能顯示任何內(nèi)容【整個(gè)過(guò)程是 同步進(jìn)行的】
今天 SSR 的一個(gè)問(wèn)題是它不允許組件“等待數(shù)據(jù)”。使用當(dāng)前的 API,當(dāng)您呈現(xiàn)為 HTML 時(shí),您必須為服務(wù)器上的組件準(zhǔn)備好所有數(shù)據(jù)。這意味著您必須先收集服務(wù)器上的所有數(shù)據(jù),然后才能開始向客戶端發(fā)送任何HTML。這是相當(dāng)?shù)托У摹?/span>
例如,假設(shè)您要呈現(xiàn)帶有評(píng)論的帖子。盡早顯示注釋很重要,因此您希望將它們包含在服務(wù)器 HTML 輸出中。但是您的數(shù)據(jù)庫(kù)或 API 層很慢,這是您無(wú)法控制的。現(xiàn)在你必須做出一些艱難的選擇。如果您將它們從服務(wù)器輸出中排除,則在 JS 加載之前用戶將不會(huì)看到它們。但是,如果您將它們包含在服務(wù)器輸出中,則必須延遲發(fā)送其余的 HTML(例如,導(dǎo)航欄、側(cè)邊欄,甚至是帖子內(nèi)容),直到評(píng)論加載完畢并且您可以呈現(xiàn)完整的樹。這不是很友好。
作為旁注,一些數(shù)據(jù)獲取解決方案反復(fù)嘗試將樹渲染為 HTML 并丟棄結(jié)果,直到數(shù)據(jù)得到解析,因?yàn)?React 沒(méi)有提供更符合人體工程學(xué)的選項(xiàng)。我們希望提供一種不需要如此極端妥協(xié)的解決方案。
2. 給任何板塊補(bǔ)水之前,您必須加載所有數(shù)據(jù)
在您的 JavaScript 代碼加載后,您將告訴 React “水合” HTML 并使其具有交互性。React 將在渲染組件時(shí)“遍歷”服務(wù)器生成的 HTML,并將事件處理程序附加到該 HTML。為此,瀏覽器中組件生成的樹必須與服務(wù)器生成的樹相匹配。否則 React 無(wú)法“匹配它們!” 這樣做的一個(gè)非常不幸的后果是,您必須先為客戶端上的所有組件加載 JavaScript,然后才能開始對(duì)它們中的任何一個(gè)進(jìn)行補(bǔ)水。
例如,假設(shè)評(píng)論小部件包含很多復(fù)雜的交互邏輯,為其加載 JavaScript 需要一段時(shí)間。現(xiàn)在你必須再次做出艱難的選擇。最好將服務(wù)器上的評(píng)論呈現(xiàn)為 HTML,以便盡早將它們顯示給用戶。但是因?yàn)榻裉熘荒芤淮瓮瓿裳a(bǔ)水,所以在您加載評(píng)論小部件的代碼之前,您無(wú)法開始對(duì)導(dǎo)航欄、側(cè)邊欄和帖子內(nèi)容進(jìn)行補(bǔ)水!當(dāng)然,您可以使用代碼拆分并單獨(dú)加載它,但是您必須從服務(wù)器 HTML 中排除注釋。否則 React 將不知道如何處理這塊 HTML(它的代碼在哪里?)并在水化過(guò)程中將其刪除。
3. 在與任何事物交互之前,您必須先補(bǔ)充所有水分
融合作用本身也存在類似的問(wèn)題。今天,React 一次性完成樹的水化。這意味著一旦它開始 hydrating(本質(zhì)上是調(diào)用你的組件函數(shù)),React 不會(huì)停止,直到它為整個(gè)樹完成此操作。因此,您必須等待所有組件都 “融合” 后才能與它們中的任何一個(gè)進(jìn)行交互。
例如,假設(shè)評(píng)論小部件具有昂貴的渲染邏輯。它可能在您的計(jì)算機(jī)上運(yùn)行得很快,但在運(yùn)行所有這些邏輯的低端設(shè)備上并不便宜,甚至可能會(huì)鎖定屏幕幾秒鐘。當(dāng)然,理想情況下,我們根本不會(huì)在客戶端上有這樣的邏輯(服務(wù)器組件可以提供幫助)。但是對(duì)于某些邏輯來(lái)說(shuō),這是不可避免的,因?yàn)樗鼪Q定了附加的事件處理程序應(yīng)該做什么并且對(duì)于交互性至關(guān)重要。因此,一旦水化開始,用戶就無(wú)法與導(dǎo)航欄、側(cè)邊欄或帖子內(nèi)容進(jìn)行交互,直到整個(gè)樹被水化。對(duì)于導(dǎo)航,這尤其令人遺憾,因?yàn)橛脩艨赡芟M耆x開此頁(yè)面——但由于我們正忙于補(bǔ)充水分,我們將它們保留在他們不再關(guān)心的當(dāng)前頁(yè)面上。
這些問(wèn)題之間有一個(gè)共同點(diǎn)。它們迫使你在早點(diǎn)做某事(但因?yàn)樗柚顾衅渌ぷ鞫鴵p害用戶體驗(yàn))或晚做某事(但因?yàn)槟憷速M(fèi)時(shí)間而損害用戶體驗(yàn))之間做出選擇。
這是因?yàn)橛幸粋€(gè)過(guò)程:獲取數(shù)據(jù)(服務(wù)器)→ 渲染到 HTML(服務(wù)器)→ 加載代碼(客戶端)→ 水合物(客戶端)。在應(yīng)用程序的前一階段完成之前,這兩個(gè)階段都不能開始。這就是它效率低下的原因。我們的解決方案是將工作分開,以便我們可以為屏幕的一部分而不是整個(gè)應(yīng)用程序執(zhí)行每個(gè)階段。
這不是一個(gè)新穎的想法:例如,
Marko[https://tech.ebayinc.com/engineering/async-fragments-rediscovering-progressive-html-rendering-with-marko/]?是實(shí)現(xiàn)此模式版本的 JavaScript Web 框架之一。挑戰(zhàn)在于如何使這樣的模式適應(yīng) React 編程模型。花了一段時(shí)間才解決。我們
讓我們看看如何
Suspense 解鎖的 React 18 中有兩個(gè)主要的 SSR 特性:
在服務(wù)器上流式傳輸 HTML。要選擇使用它,您需要renderToString從新pipeToNodeWritable方法切換到新方法,如此處所述。
對(duì)客戶進(jìn)行選擇性水合作用。要選擇加入它,您需要在客戶端上切換到createRoot,然后開始使用
。( https://github.com/reactwg/react-18/discussions/5)
要了解這些功能的作用以及它們?nèi)绾谓鉀Q上述問(wèn)題,讓我們返回到我們的示例。
<main><nav><a href="/">Homea>nav><aside><a href="/profile">Profilea>aside><article><p>Hello worldp>article><section><p>First commentp><p>Second commentp>section>main>


例如,讓我們包裝注釋塊并告訴 React,在它準(zhǔn)備好之前,React 應(yīng)該顯示該
<NavBar /><Sidebar /><RightPane><Post /><Suspense fallback={<Spinner />}><Comments />Suspense>RightPane>Layout>
將

<main><nav><a href="/">Homea>nav><aside><a href="/profile">Profilea>aside><article><p>Hello worldp>article><section id="comments-spinner"><img width=400 src="spinner.gif" alt="Loading..." />section>main>
故事到這里還沒(méi)有結(jié)束。當(dāng)評(píng)論的數(shù)據(jù)在服務(wù)器上準(zhǔn)備好時(shí),React會(huì)將額外的 HTML 發(fā)送到同一個(gè)流中,以及一個(gè)最小的內(nèi)聯(lián)
