我們一起理解 React 服務端組件
有件事讓我感覺自己真的老了:React 今年已經 10 歲了。
自從 React 首次被引入以來,經歷了幾次演變。 React 團隊并不羞于改變:如果他們發現了更好的問題解決方案,就會采用。
React 團隊推出了 React 服務端組件(React Server Components),這是最新的編寫范式。 React 組件有史以來第一次可以專門在服務器上運行。
網上對這個概念有太多不理解。許多人對服務端組件是什么、如何工作、有什么好處以及是如何與服務器端渲染等內容結合使用存在很多疑問。
我一直在使用 React 服務端組件進行大量實驗,也回答了我自己產生的很多問題。我必須承認,我對這些東西比我預想的要興奮得多,因為它真的很酷!
今天,我將幫助你揭開 React 服務端組件的神秘面紗,回答你可能對 React 服務端組件存在的許多問題!
服務端渲染快速入門
由于實際場景中,React 服務端組件通常與服務端渲染(Server Side Rendering,簡稱 SSR)配合使用,因此預先了解服務端渲染的工作原理會很有幫助。當然,如果你已經很熟悉 SSR 了,則可以跳過本節的學習。
在我 2015 年第一次使用 React 時,那時候的大多數 React 項目都還采用“客戶端渲染”策略。
在客戶端渲染模式下,用戶會先收到下面這樣一個比較簡單的網頁。
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="/static/js/bundle.js"></script>
</body>
</html>bundle.js 包含整個項目初始化和運行階段的所有代碼。包括 React、其他三方依賴以及我們自己的業務代碼。
JS 文件下載并解析后,React 會立即介入,準備好渲染應用所需要的 DOM 節點,并插入到空的 <div id="root"> 里。到這里,用戶就得到可以交互的頁面了。
雖然這個空的 HTML 文檔會很快接收,但 JS 文件的下載和解析是需要一些時間的,另外隨著我們項目規模的擴大,JS 文件本身的體積可能也在不斷變大。
在客戶端接收到 HTML 文檔,到 JS 文件處理結束的中間階段,用戶通常會面臨白屏問題,這種體驗就比較糟糕了。
服務端渲染就能有效的避免這種體驗。服務端渲染會將我們首屏要展示的 HTML 內容在服務端預先生成,再發送到客戶端。這樣,客戶端在接收到 HTML 時,就能渲染首屏內容,也就不會遇到白屏問題了。
當然,服務端渲染的 HTML 網頁同樣會包含 <script> 標簽,因為發送的首屏內容還需要交由 React 托管,附加交互能力。具體來說:與客戶端從頭構建 DOM 不同,服務端渲染模式下,React 會利用現有的 HTML 結構進行構建,并為 DOM 節點附加交互能力,以便響應用戶操作。這個過程被稱為“水合(hydration)”。
我很喜歡 React 核心團隊成員 Dan Abramov 對這一過程的通俗解釋:
水合(Hydration)就類似使用交互和事件處理程序的“水”澆到“干”的 HTML 上。
JS 包下載后,React 將快速運行我們的整個應用程序,構建 UI 的虛擬草圖,并將其“擬合”到真實的 DOM 節點、附加事件處理程序、觸發 effect 等。
簡而言之,SSR 就是服務器生成初始 HTML,這樣用戶在等待 JS 處理過程中,不會看到白屏。另外,客戶端 React 會接手服務器端 React 的工作,為 DOM 加入交互能力。
?? 關于靜態站點生成
當我們談論服務器端渲染時,我們通常想到的可能是下面的流程:
- 用戶訪問 myWebsite.com
- Node.js 服務器接收請求,并立即渲染 React 應用程序,生成 HTML
- 服務端生成的 HTML 被發送到客戶端
這是實現服務器端渲染的一種可能方法,但不是唯一的方法。另一種選擇是在構建(build)應用程序時生成 HTML。
通常,React 應用程序需要進行編譯,將 JSX 轉換為普通的 JavaScript,并打包我們的所有模塊。如果在這一過程中,我們為所有不同的路由“預渲染”所有 HTML 如何?
這種做法通常稱為靜態站點生成 (static site generatio,簡稱 SSG),它是服務器端渲染的一個變體。
在我看來,“服務器端渲染”是一個通用術語,包括幾種不同的渲染策略。不過,都有一個共同點:初始渲染都是使用 ReactDOMServer API,發生在 Node.js 等服務器運行時環境。
現有渲染方案分析
本節我們再來談談 React 中的數據獲取。通常,我們有兩個通過網絡進行通信的獨立應用程序:
- 客戶端 React 應用程序
- 服務器端 REST API
在客戶端我們使用類似 React Query、SWR 或 Apollo 這樣的工具向后端發起網絡請求,從后端數據庫中獲取數據并通過網絡發送回來。
我們可以將這一過程可視化成下面這樣。
圖片
這里就展示了客戶端渲染 (CSR) 的工作流程。從客戶端接收到 HTML 開始。這個 HTML 文檔不包含任何內容,但會有一個或多個 <script> 標簽。
JS 文件下載并解析好后,React 應用程序將啟動,創建一堆 DOM 節點并填充 UI。不過,一開始我們沒有任何實際數據,因此往往會使用一個骨架屏來表示處于加載狀態中,這一階段稱為“Render Shell”,也就是“渲染骨架屏”。
這種模式很常見了。以 UberEats 網站舉例,在獲取到實際數據前,會展示下面的加載效果。
圖片
在獲取實際數據并替換當前內容前,用戶會一直看到這個加載頁面。
以上就是典型的客戶端渲染方案。再來看看服務端渲染方案的執行流程。
圖片
可以看到,“Render Shell”階段被放在了服務端,也就是說用戶收到就不是空白 HTML 了,這是比客戶端渲染好一點的地方,至少沒有白屏了。
為了方便比較,我們在圖標中有增加了一些常用網絡性能指標。看看在這兩個流程之間切換,有哪些指標發生了改變。
圖片
圖表中這些 Web 性能指標的介紹如下:
- First Paint(首次繪制):因為總體布局在服務端渲染了,所以用戶不會看到白屏了。這個指標還叫 First Contentful Paint,即首次內容繪制,簡稱 FCP
- Page Interactive:React 下載好了,應用也經過渲染、水合處理了,現在頁面元素能夠響應交互了。這個指標還叫 Time To Interactive,即可交互時間,簡稱 TTI
- Content Paint:用戶想看的內容在頁面中出現了。也也就說我們從數據庫中拿到的數據在頁面中成功渲染了。這個指標還叫 Largest Contentful Paint,即最大內容繪制,簡稱 LCP
通過在服務器上進行初始渲染,我們能夠更快地繪制初始“Shell”頁面,即“骨架屏”頁面。體驗上會感覺更快一些,因為它提供了一種響應標識,告訴你頁面正在渲染。
某些情況下,這將是一個有意義的改進。但這樣的流程會感覺有點傻,用戶訪問我們的應用程序不是為了查看加載屏幕,而是為了查看內容。
當再次查看 SSR 圖時,我不禁想到如果把數據庫請求也放在服務器上執行,那么我們不就可以避免客戶端網頁的網絡請求了嗎?
換句話說,也就是下面這樣。
圖片
我們不會在客戶端和服務器之間來回切換,當數據庫查詢結果作為初始請求的一部分時,在客戶端接收到的 HTML 文檔中,就包含用戶向看到的內容了。
不過,我們該怎么做呢?
React 并沒有提供這方面渲染方案的支持,不過生態系統針對這個問題提出了很多解決方案。像 Next.js 和 Gatsby 這樣的元框架(Meta Frameworks)就創造了自己的方式來專門在服務器上運行代碼。
以 Next.js 為例(使用舊的 Pages Router 模式):
import db from 'imaginary-db';
// This code only runs on the server:
export async function getServerSideProps() {
const link = db.connect('localhost', 'root', 'passw0rd');
const data = await db.query(link, 'SELECT * FROM products');
return {
props: { data },
};
}
// This code runs on the server + on the client
export default function Homepage({ data }) {
return (
<>
<h1>Trending Products</h1>
{data.map((item) => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</>
);
}這里簡單介紹下:當服務器收到請求時,會先調用 getServerSideProps 函數,它返回一個 props 對象。接著,這些 props 被傳給組件,這個組件會先使用這些 props 在服務器上進行一次渲染,然后將結果發送到客戶端,最后在客戶端進行水合。
getServerSideProps 是一個特殊的函數,只在服務器端執行,函數本身也不會包含在發送給客戶端的 JavaScript 文件中。
這種方法在當時是非常超前的,但也有一些缺點:
- 這個策略僅適用于路由級別的組件,也就是在整個頁面組件樹的最頂部的這個組件,而對后代子組件無法適用
- 這個策略并沒有標準化,導致每個元框架的具體實現各不相同。Next.js 是一種,Gatsby 則是另一種,Remix 再是一種
- 所有的 React 組件都會在客戶端上進行一次水合,即便組件本身可能并不需要(比如:沒有任何交互功功能、只是用于純展示作用的組件)
當然,React 團隊也意識到了這個問題,并一直嘗試給出一個官方方案。最終,方案確定了下來,也就是我們看到的 React Server Components,即 React 服務端組件,簡稱 RSC。
React 服務端組件介紹
React 服務端組件是一個全新的渲染模式,在這個模式下,組件完全在服務器上運行,讓我們可以組件中做類似查詢數據庫的后端操作。
下面是一個“服務端組件”的簡單示例。
import db from 'imaginary-db';
async function Homepage() {
const link = db.connect('localhost', 'root', 'passw0rd');
const data = await db.query(link, 'SELECT * FROM products');
return (
<>
<h1>Trending Products</h1>
{data.map((item) => (
<article key={item.id}>
<h2>{item.title}</h2>
<p>{item.description}</p>
</article>
))}
</>
);
}
export default Homepage;如果你已經寫了很多年的 React,這樣的代碼一定會讓你感覺奇怪 ??。
我就是其中之一。當我看到這種寫法時,本能地驚嘆道。 “函數組件不能異步呀!而且我們不能直接在渲染中出現這樣的副作用!”
這里要理解的關鍵點是:服務端組件只會渲染一次,永遠不會重新渲染。它們在服務器上運行一次生成 UI,并將渲染的值發送到客戶端并原地鎖定,輸出永遠不會改變。
這表示 React 的 API 的很大一部分與服務端組件是不兼容的。例如,我們不能使用 useSate(),因為狀態可以改變,但服務端組件不支持重新渲染。我們不能使用 useEffect(),因為它只在渲染后在客戶端上運行,而服務端組件是不會發送到客戶端的。
不過,由于服務端環境限制,也給服務端組件的編寫帶來一定靈活性。例如:在傳統客戶端 React 中,我們需要將副作用放入 useEffect() 回調或事件處理程序中,避免每次渲染時重復調用。但如果組件本身只運行一次,我們就不必擔心這個問題了!
服務端組件本身非常簡單,但“React 服務端組件”模式要復雜得多。這是因為我們還要支持以前的常規組件,混用就會帶來混亂。
為了與新的“React 服務端組件”做區分,傳統 React 組件被稱為“客戶端組件(Client Component)”。老實說,我不是很喜歡這個名字。
“客戶端組件”聽起來好像這些組件只在客戶端上渲染,實際上并非如此——客戶端組件在客戶端和服務器端都會渲染。
圖片
我知道所有這些術語都非常令人困惑,所以我做了一下總結:
- React 服務端組件(React Server Components)是這個新模式的名稱
- 我們所了解的“標準”React 組件被重新命名為客戶端組件(Client Component),這是對舊事物的一個新稱呼
- 這個新模式引入了一個新的類型組件:服務端組件(Server Component),這些組件專門在服務器上渲染,其代碼也不會包含在發送給客戶端的 JS Bundle 中,因此也不會參與水合或重新渲染
?? 服務端組件與服務器端渲染
這里必須要澄清一下:React 服務端組件并不是服務器端渲染的替代品。你不應該把 React Server Components 理解成“SSR 的 2.0 版本”
這 2 者更像是可以拼湊在一起的拼圖,相輔相成。
我們仍然需要服務器端渲染來生成初始 HTML。React Server Components 則是建立在基礎之上,讓我們從客戶端 JavaScript 包中省略這些組件,確保它們只在服務器上運行。
事實上,你也可以在沒有服務器端渲染的情況下使用 React 服務端組件。實踐中它們通常一起使用,來得到更好的結果。如果你想查看示例,React 團隊已經構建了一個沒有 SSR 的最小 RSC demo[2]。
在使用服務端組件之前
通常,當新的 React 功能出現時,我們可以通過將 React 依賴項升級到最新版本來使用,類似 npm install react@latest 就可以了,不過服務端組件不是這樣。
我的理解是:服務端組件需要與 React 之外的一些系統緊密配合才能使用,比如打包工具(bundler)、服務器、路由之類的。
當我寫這篇文章時,Next.js 13.4+ 通過引入全新的重新架構“App Router” 來支持服務端組件的使用。
當然,在可以遇見的將來,會有越來越多的基于 React 的框架會支持這一特性。React 官方文檔有一個 “Bleeding-edge frameworks”[3] 的部分,其中列出了支持 React 服務端組件的框架列表。
使用客戶端組件
在 Next.js App Router 架構下,默認所有組件都會被看作服務端組件,客戶端組件需要特別聲明,這需要通過一個新的指令說明。
'use client';
import React from 'react';
function Counter() {
const [count, setCount] = React.useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Current value: {count}
</button>
);
}
export default Counter;注意,這里頂部的 'use client',這就是在告訴 React 這是一個客戶端組件,應該包含在 JS Bundle 中,以便在客戶端上重新渲染。
這種聲明方式借鑒了 JavaScript 的嚴格模式聲明——'use strict'。
在 App Router 架構下,所有組件默認被看作是服務端組件,無需任何聲明。當然,你可能會想到服務端組件是不是使用 'use server'——NO,不是!'use server' 其實是用在 Server Actions,而非服務端組件上的,不過這塊內容超出了本文范圍就不講了,有興趣的同學可以私下學習。
?? 哪些組件應該是客戶端組件?
這里你可能就有疑問了:我該怎么知道一個組件應該是服務端組件還是客戶端組件呢?
這里可以給大家一個一般規則:如果一個組件可以是服務端組件,那么它就應該是服務端組件。服務端組件往往更簡單且更容易推理,還有一個性能優勢,即服務端組件不在客戶端上運行,所以它們的代碼不包含在我們的 JavaScript 包中。因此,React 服務端組件對改進頁面交互指標(TTI)有所幫助。
不過,這不意味著我們要盡可能把作為組件都改成服務端組件,不合理也不可能。在 RSC 之前,每個 React 應用程序中的 React 組件都是客戶端組件。
當你開始使用 React 服務端組件時,你會發現它寫起來這非常直觀。而我們的一些組件由于需要狀態或 Effect,只能在客戶端上運行。你可以通過在組件頂部添加 'use client' 指令指定當前組件是客戶端組件,否則默認就是服務端組件。
客戶端邊界
當我熟悉 React 服務端組件時,我遇到的第一個問題是:如果組建 props 改變了,會發生什么?
假設,我們有一個像這樣的服務端組件:
function HitCounter({ hits }) {
return (
<div>
Number of hits: {hits}
</div>
);
}如果在初始服務器端渲染中, hits 等于 0 。然后,這個組件將生成以下結果。
<div>
Number of hits: 0
</div>但是,如果 hits 的值發生變化會怎樣?假設它是一個狀態變量,從 0 更成了 1。HitCounter 這個時候就需要重新渲染,但它不能重新渲染,因為它是服務端組件!
這里的問題是,如果沒有上下文環境,只是孤立的考慮服務端組件并沒有真正的意義。我們必須擴大范圍,從更高的角度審視,考慮我們應用程序的結構。
假設我們有如下的組件樹結構:
圖片
如果所有這些組件都是服務端組件,那么就不會存在上面的問題,因為所有組件都不會重新渲染,props 也就沒有改變的可能性。
但假設 Article 組件擁有 hits 狀態變量。為了使用狀態,我們需要將其轉換為客戶端組件:
圖片
你觀察到這里的問題了嗎?當 Article 重新渲染時,任何下屬子組件也會重新渲染,包括 HitCounter 和 Discussion。但是,如果這些是服務端組件,是無法重新渲染的。
為了避免這類矛盾場景的出現,React 團隊添加了一條規則:客戶端組件只能導入其他客戶端組件。'use client' 指令表示 HitCounter 和 Discussion 的這些實例將自動成為客戶端組件。
我在使用 React 服務端組件時遇到的最大的“啊哈(ah-ha)”時刻之一,是意識到服務端組件的這種新模式其實就是關于創建客戶端邊界的(client boundaries)。在實踐中,總會遇到下面的場景:
圖片
當我們將 'use client' 指令添加到 Article 組件時,我們創建了一個“客戶端邊界”。邊界內的所有組件都隱式成為客戶端組件。即使像 HitCounter 這樣的組件沒有使用 'use client' 指令,在這種特殊情況下它們仍然會在客戶端上進行水合和渲染。
也就是說,我們不必將 'use client' 添加到每個客戶端上運行的組件,只需要在創建新的客戶端邊界的組件上添加即可。
解決服務端組件帶來的限制問題
當我第一次了解到客戶端組件無法渲染服務端組件時,它對我來說感覺非常限制。如果我需要在應用程序中使用高層狀態怎么辦?那所有組件豈不是都成為客戶端組件了?
事實證明,在許多情況下,我們可以通過重構組件來解決這個限制。
這是一件很難解釋的事情,所以讓我們先舉個例子說明:
'use client';
import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';
import Header from './Header';
import MainContent from './MainContent';
function Homepage() {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_COLORS
: DARK_COLORS;
return (
<body style={colorVariables}>
<Header />
<MainContent />
</body>
);
}在這段代碼中,我們需要使用 React 狀態允許用戶在深色/淺色模式之間切換。這類功能通常需要在應用程序樹的較高層級設置,以便我們可以將 CSS 變量 token 應用到 <body> 上。
為了使用狀態,我們需要讓 Homepage 成為客戶端組件。由于這是我們應用程序的頂部,表示其他所有組件 - Header 和 MainContent - 也將隱式成為客戶端組件。
為了解決這個問題,讓我們將主題管理提取到單獨的組件文件中:
// /components/ColorProvider.js
'use client';
import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';
function ColorProvider({ children }) {
const [colorTheme, setColorTheme] = React.useState('light');
const colorVariables = colorTheme === 'light'
? LIGHT_COLORS
: DARK_COLORS;
return (
<body style={colorVariables}>
{children}
</body>
);
}返回 HomaPage,就可以像這樣重新組織了:
// /components/Homepage.js
import Header from './Header';
import MainContent from './MainContent';
import ColorProvider from './ColorProvider';
function Homepage() {
return (
<ColorProvider>
<Header />
<MainContent />
</ColorProvider>
);
}現在就可以從 Homepage 中刪除 'use client' 指令了,因為它不再使用狀態或任何其他客戶端 React 功能,也就表示 Header 和 MainContent 不再需要被迫轉換成客戶端組件了!
當然,你可能會有疑問了。ColorProvider 是一個客戶端組件,是 Header 和 MainContent 的父組件。不管怎樣,它仍然處在樹結構的較高層級,是吧?
確實。不過,Header 和 MainContent 是在 Homepage 中引入的,這表示它們的 props 只受到 HomaPage 影響。也就是說,客戶端邊界只對邊界頂部組件的內部有影響,對同處于一個父組件下的其他組件沒有影響。
請記住,我們試圖解決的問題是服務端組件無法重新渲染的問題,因此無法為它們的任何子組件設置新的 props。Homepage 決定 Header 和 MainContent 的 props 是什么,并且由于 Homepage 本身是一個服務端組件,那么同屬于服務端組件的 Header、MainContent 自然就沒有 props 會改變的擔憂。
不得不承認的是,理解服務端組件架構確實是一件費腦筋的事情。即使有了多年的 React 經驗,我仍然覺得這很令人困惑,需要相當多的練習才能培養對這種新架構的直覺。
更準確地說,'use client' 指令是在文件/模塊級別下工作的。客戶端組件中導入的任何模塊也必須是客戶端組件。畢竟,當打包工具打包我們的代碼時,也是依據這些導入聲明一同打包的!
淺析底層實現
現在讓我們從一個較低的層面來看服務端組件的實現。當我們使用服務端組件時,輸出是什么樣的?實際生成了什么?
讓我們從一個超級簡單的 React 應用程序開始:
function Homepage() {
return (
<p>
Hello world!
</p>
);
}在 Next.js App Router 模式下,所有組件默認都是服務端組件。也就是說,Homepage 就是服務端組件,會在服務端渲染。
當我們在瀏覽器中訪問此應用程序時,我們將收到一個 HTML 文檔,如下所示:
<!DOCTYPE html>
<html>
<body>
<p>Hello world!</p>
<script src="/static/js/bundle.js"></script>
<script>
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};
</script>
</body>
</html>我們看到 HTML 文檔包含由 React 應用程序生成的 UI,即“Hello world!”段落。其實這屬于服務器端渲染結果,跟 React 服務端組件沒有關系。
再往下,是一個 <script> 標簽來加載我們的 JS 包。這個腳本中包括 React 等依賴項,以及我們應用程序中使用的所有客戶端組件代碼。由于我們的 Homepage 是服務端組件,所以這個組件的代碼不包含在這個 JS 包中。
最后,第二個 <script> 標簽,其中包含一些內聯 JS:
self.__next['$Homepage-1'] = {
type: 'p',
props: null,
children: "Hello world!",
};這里就比較有趣了。本質上這里所做的就是告訴 React——“嘿,我知道你看不到 Homepage 組件代碼,但不用擔心:這就是它渲染的內容”。通常來說,當 React 在客戶端上水合時,這種做法會加速整個渲染進程,因為部分組件(服務端組件)已經在后端渲染出來了,其組件代碼也不會包含在 JS 文件中。
我們會將服務器生成的虛擬表示發送回去,當 React 在客戶端加載時,它會重用這這部分虛擬描述,而不是重新生成它。
這就是上面的 ColorProvider 能夠工作的原因。 Header 和 MainContent 的輸出通過 children 屬性傳遞到 ColorProvider 組件。ColorProvider 可以根據需要重新渲染,但數據是靜態的,在服務器就鎖定了。
如果你想了解服務端組件如何序列化并通過網絡發送的,可以使用 Alvar Lagerl?f 開發的 RSC Devtools[4] 進行查看。
?? 服務端組件不需要服務器
我們有一道,服務器端渲染其實是很多不同渲染策略的總稱。包括:
- 靜態的:HTML 是在構建階段生成的
- 動態的:HTML 是在用戶請求是生成的,即“按需”生成的
React Server Components 與上述這 2 渲染策略都是兼容的。當服務端組件在 Node.js 調用渲染時,會返回的當前組件的 JavaScript 對象表示。這個操作可以在構建時,也可以在請求時。
也就是說,在沒有服務器的情況下使用 React 服務端組件!我們可以生成一堆靜態 HTML 文件并將它們托管在某個地方,事實上,這就是 Next.js App Router 中默認就是這個策略——除非我們真的需要推遲到“請求”階段,否則所有這些工作都會在構建期間提前發生。
服務端組件的好處
React 服務端組件比較酷的一點就在于:它是 React 中運行服務器專有代碼的第一個“官方”方案。另外,自 2016 年以來,我們已經能夠在 Next.js 的 App Router 模式下使用服務端組件了!
不過,這種方案引入之后,編寫 React 代碼的方式變得很不一樣了,因為我們需要編寫專用于服務端的 React 的代碼了。
這樣帶來的一個最明顯好處就是性能了。服務端組件不包含在我們發送給客戶端的 JS 包中,這樣就減少了需要下載的 JS 代碼數量以及需要水合的組件數量:
圖片
不過,這對我來說可能是最不令人興奮的事情。畢竟,大多數 Next.js 應用程序在“頁面可交互(Page Interactive)”方面已經做得足夠快了。
如果你遵循語義 HTML 原則,那么你的大部分應用程序甚至在 React 水合之前就可以運行。比如:跳轉鏈接、提交表單、展開和折疊手風琴(使用 <details> 和 <summary>)等。者對于大多數項目來說,React 只需要幾秒鐘的時間來進行水合就很不錯了。
不過,React 服務端組件真正的優勢在于,我們不再需要在功能與打包文件尺寸上妥協了!
例如,大多數技術博客都需要某種語法高亮庫。在我的博客里,我使用 Prism。代碼片段如下所示:
function exampleJavaScriptFunction(param) {
return "Hello world!"
}一個流行語法高亮庫,通常會支持很多流行的編程語言,有幾兆字節,放到 JS 包中實在太大。因此,我們必須做出妥協,刪除非必須語言和功能。
但是,假設我們在服務端組件中進行語法突出顯示。在這種情況下,我們的 JS 包中實際上不會包含高亮庫代碼。因此,我們不必做出任何妥協,另外我們還可以使用所有的附加功能。
Bright[5] 就是支持在服務端組件中使用的現代語法高亮庫。
圖片
這是讓我對 React 服務端感到興奮的一個地方。原本包含在 JS 包中成本太高的東西現在可以在服務器上運行,而不必在包含在 JS 包中了,這也帶來了更好的用戶體驗。
這也不僅僅是性能和用戶體驗。使用 RSC 一段時間后,我開始真正體會到服務端組件是多么簡單易用。我們永遠不必擔心依賴數組、過時的閉包、記憶或由事物變化引起的任何其他復雜的東西。
我真的很高興看到未來幾年事情將如何發展,因為社區將利用這種新模式繼續創造出像 Bright 這樣新的解決方案。對于成為一名 React 開發者來說,這很令人激動!
完整圖表
React 服務端組件是一項令人興奮的方案,但它實際上只是“現代 React”難題的一部分。
當我們將 React 服務端組件與 Suspense 和新的 Streaming SSR 架構結合起來時,事情變得更加有趣。它允許我們做下面這樣瘋狂的事情:
圖片
簡單來說,內置 Suspense 組件能夠利用 Streaming SSR + React 服務端組件架構實現局部組件更新。這樣每塊內容都可以單獨渲染、處理,能更快響應用戶,帶來更好地瀏覽體驗。
不過這部分知識超出了本文范圍,你可以在 Github[6] 上了解有關此架構的更多信息。
參考資料
[1]
Making Sense of React Server Components: https://www.joshwcomeau.com/react/server-components/
[2]最小 RSC demo: https://github.com/reactjs/server-components-demo
[3]“Bleeding-edge frameworks”: https://react.dev/learn/start-a-new-react-project#bleeding-edge-react-frameworks
[4]RSC Devtools: https://www.alvar.dev/blog/creating-devtools-for-react-server-components
[5]Bright: https://bright.codehike.org/
[6]Github: https://github.com/reactwg/react-18/discussions/37
































