自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我們一起理解 React 服務(wù)端組件

開發(fā) 前端
簡單來說,內(nèi)置 Suspense 組件能夠利用 Streaming SSR + React 服務(wù)端組件架構(gòu)實現(xiàn)局部組件更新。這樣每塊內(nèi)容都可以單獨渲染、處理,能更快響應(yīng)用戶,帶來更好地瀏覽體驗。

有件事讓我感覺自己真的老了:React 今年已經(jīng) 10 歲了。

自從 React 首次被引入以來,經(jīng)歷了幾次演變。 React 團隊并不羞于改變:如果他們發(fā)現(xiàn)了更好的問題解決方案,就會采用。

React 團隊推出了 React 服務(wù)端組件(React  Server Components),這是最新的編寫范式。 React 組件有史以來第一次可以專門在服務(wù)器上運行。

網(wǎng)上對這個概念有太多不理解。許多人對服務(wù)端組件是什么、如何工作、有什么好處以及是如何與服務(wù)器端渲染等內(nèi)容結(jié)合使用存在很多疑問。

我一直在使用 React 服務(wù)端組件進行大量實驗,也回答了我自己產(chǎn)生的很多問題。我必須承認,我對這些東西比我預(yù)想的要興奮得多,因為它真的很酷!

今天,我將幫助你揭開 React 服務(wù)端組件的神秘面紗,回答你可能對 React 服務(wù)端組件存在的許多問題!

服務(wù)端渲染快速入門

由于實際場景中,React 服務(wù)端組件通常與服務(wù)端渲染(Server Side Rendering,簡稱 SSR)配合使用,因此預(yù)先了解服務(wù)端渲染的工作原理會很有幫助。當然,如果你已經(jīng)很熟悉 SSR 了,則可以跳過本節(jié)的學(xué)習(xí)。

在我 2015 年第一次使用 React 時,那時候的大多數(shù) React 項目都還采用“客戶端渲染”策略。

在客戶端渲染模式下,用戶會先收到下面這樣一個比較簡單的網(wǎng)頁。

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>

bundle.js 包含整個項目初始化和運行階段的所有代碼。包括 React、其他三方依賴以及我們自己的業(yè)務(wù)代碼。

JS 文件下載并解析后,React 會立即介入,準備好渲染應(yīng)用所需要的 DOM 節(jié)點,并插入到空的 <div id="root"> 里。到這里,用戶就得到可以交互的頁面了。

雖然這個空的 HTML 文檔會很快接收,但 JS 文件的下載和解析是需要一些時間的,另外隨著我們項目規(guī)模的擴大,JS 文件本身的體積可能也在不斷變大。

在客戶端接收到 HTML 文檔,到 JS 文件處理結(jié)束的中間階段,用戶通常會面臨白屏問題,這種體驗就比較糟糕了。

服務(wù)端渲染就能有效的避免這種體驗。服務(wù)端渲染會將我們首屏要展示的 HTML 內(nèi)容在服務(wù)端預(yù)先生成,再發(fā)送到客戶端。這樣,客戶端在接收到 HTML 時,就能渲染首屏內(nèi)容,也就不會遇到白屏問題了。

當然,服務(wù)端渲染的 HTML 網(wǎng)頁同樣會包含 <script> 標簽,因為發(fā)送的首屏內(nèi)容還需要交由 React 托管,附加交互能力。具體來說:與客戶端從頭構(gòu)建 DOM 不同,服務(wù)端渲染模式下,React 會利用現(xiàn)有的 HTML 結(jié)構(gòu)進行構(gòu)建,并為 DOM 節(jié)點附加交互能力,以便響應(yīng)用戶操作。這個過程被稱為“水合(hydration)”。

我很喜歡 React 核心團隊成員 Dan Abramov 對這一過程的通俗解釋:

水合(Hydration)就類似使用交互和事件處理程序的“水”澆到“干”的 HTML 上。

JS 包下載后,React 將快速運行我們的整個應(yīng)用程序,構(gòu)建 UI 的虛擬草圖,并將其“擬合”到真實的 DOM 節(jié)點、附加事件處理程序、觸發(fā) effect 等。

簡而言之,SSR 就是服務(wù)器生成初始 HTML,這樣用戶在等待 JS 處理過程中,不會看到白屏。另外,客戶端 React 會接手服務(wù)器端 React 的工作,為 DOM 加入交互能力。

?? 關(guān)于靜態(tài)站點生成

當我們談?wù)摲?wù)器端渲染時,我們通常想到的可能是下面的流程:

  1. 用戶訪問 myWebsite.com
  2. Node.js 服務(wù)器接收請求,并立即渲染 React 應(yīng)用程序,生成 HTML
  3. 服務(wù)端生成的 HTML 被發(fā)送到客戶端

這是實現(xiàn)服務(wù)器端渲染的一種可能方法,但不是唯一的方法。另一種選擇是在構(gòu)建(build)應(yīng)用程序時生成 HTML。

通常,React 應(yīng)用程序需要進行編譯,將 JSX 轉(zhuǎn)換為普通的 JavaScript,并打包我們的所有模塊。如果在這一過程中,我們?yōu)樗胁煌穆酚伞邦A(yù)渲染”所有 HTML 如何?

這種做法通常稱為靜態(tài)站點生成 (static site generatio,簡稱 SSG),它是服務(wù)器端渲染的一個變體。

在我看來,“服務(wù)器端渲染”是一個通用術(shù)語,包括幾種不同的渲染策略。不過,都有一個共同點:初始渲染都是使用 ReactDOMServer API,發(fā)生在 Node.js 等服務(wù)器運行時環(huán)境。

現(xiàn)有渲染方案分析

本節(jié)我們再來談?wù)?React 中的數(shù)據(jù)獲取。通常,我們有兩個通過網(wǎng)絡(luò)進行通信的獨立應(yīng)用程序:

  • 客戶端 React 應(yīng)用程序
  • 服務(wù)器端 REST API

在客戶端我們使用類似 React Query、SWR 或 Apollo 這樣的工具向后端發(fā)起網(wǎng)絡(luò)請求,從后端數(shù)據(jù)庫中獲取數(shù)據(jù)并通過網(wǎng)絡(luò)發(fā)送回來。

我們可以將這一過程可視化成下面這樣。

圖片圖片

這里就展示了客戶端渲染 (CSR) 的工作流程。從客戶端接收到 HTML 開始。這個 HTML 文檔不包含任何內(nèi)容,但會有一個或多個 <script> 標簽。

JS 文件下載并解析好后,React 應(yīng)用程序?qū)樱瑒?chuàng)建一堆 DOM 節(jié)點并填充 UI。不過,一開始我們沒有任何實際數(shù)據(jù),因此往往會使用一個骨架屏來表示處于加載狀態(tài)中,這一階段稱為“Render Shell”,也就是“渲染骨架屏”。

這種模式很常見了。以 UberEats 網(wǎng)站舉例,在獲取到實際數(shù)據(jù)前,會展示下面的加載效果。

圖片圖片

在獲取實際數(shù)據(jù)并替換當前內(nèi)容前,用戶會一直看到這個加載頁面。

以上就是典型的客戶端渲染方案。再來看看服務(wù)端渲染方案的執(zhí)行流程。

圖片圖片

可以看到,“Render Shell”階段被放在了服務(wù)端,也就是說用戶收到就不是空白 HTML 了,這是比客戶端渲染好一點的地方,至少沒有白屏了。

為了方便比較,我們在圖標中有增加了一些常用網(wǎng)絡(luò)性能指標。看看在這兩個流程之間切換,有哪些指標發(fā)生了改變。

圖片圖片

圖表中這些 Web 性能指標的介紹如下:

  1. First Paint(首次繪制):因為總體布局在服務(wù)端渲染了,所以用戶不會看到白屏了。這個指標還叫 First Contentful Paint,即首次內(nèi)容繪制,簡稱 FCP
  2. Page Interactive:React 下載好了,應(yīng)用也經(jīng)過渲染、水合處理了,現(xiàn)在頁面元素能夠響應(yīng)交互了。這個指標還叫 Time To Interactive,即可交互時間,簡稱 TTI
  3. Content Paint:用戶想看的內(nèi)容在頁面中出現(xiàn)了。也也就說我們從數(shù)據(jù)庫中拿到的數(shù)據(jù)在頁面中成功渲染了。這個指標還叫 Largest Contentful Paint,即最大內(nèi)容繪制,簡稱 LCP

通過在服務(wù)器上進行初始渲染,我們能夠更快地繪制初始“Shell”頁面,即“骨架屏”頁面。體驗上會感覺更快一些,因為它提供了一種響應(yīng)標識,告訴你頁面正在渲染。

某些情況下,這將是一個有意義的改進。但這樣的流程會感覺有點傻,用戶訪問我們的應(yīng)用程序不是為了查看加載屏幕,而是為了查看內(nèi)容。

當再次查看 SSR 圖時,我不禁想到如果把數(shù)據(jù)庫請求也放在服務(wù)器上執(zhí)行,那么我們不就可以避免客戶端網(wǎng)頁的網(wǎng)絡(luò)請求了嗎?

換句話說,也就是下面這樣。

圖片圖片

我們不會在客戶端和服務(wù)器之間來回切換,當數(shù)據(jù)庫查詢結(jié)果作為初始請求的一部分時,在客戶端接收到的 HTML 文檔中,就包含用戶向看到的內(nèi)容了。

不過,我們該怎么做呢?

React 并沒有提供這方面渲染方案的支持,不過生態(tài)系統(tǒng)針對這個問題提出了很多解決方案。像 Next.js 和 Gatsby 這樣的元框架(Meta Frameworks)就創(chuàng)造了自己的方式來專門在服務(wù)器上運行代碼。

以 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>
      ))}
    </>
  );
}

這里簡單介紹下:當服務(wù)器收到請求時,會先調(diào)用 getServerSideProps 函數(shù),它返回一個 props 對象。接著,這些 props 被傳給組件,這個組件會先使用這些 props 在服務(wù)器上進行一次渲染,然后將結(jié)果發(fā)送到客戶端,最后在客戶端進行水合。

getServerSideProps 是一個特殊的函數(shù),只在服務(wù)器端執(zhí)行,函數(shù)本身也不會包含在發(fā)送給客戶端的 JavaScript 文件中。

這種方法在當時是非常超前的,但也有一些缺點:

  1. 這個策略僅適用于路由級別的組件,也就是在整個頁面組件樹的最頂部的這個組件,而對后代子組件無法適用
  2. 這個策略并沒有標準化,導(dǎo)致每個元框架的具體實現(xiàn)各不相同。Next.js 是一種,Gatsby 則是另一種,Remix 再是一種
  3. 所有的 React 組件都會在客戶端上進行一次水合,即便組件本身可能并不需要(比如:沒有任何交互功功能、只是用于純展示作用的組件)

當然,React 團隊也意識到了這個問題,并一直嘗試給出一個官方方案。最終,方案確定了下來,也就是我們看到的 React Server Components,即 React 服務(wù)端組件,簡稱 RSC。

React 服務(wù)端組件介紹

React 服務(wù)端組件是一個全新的渲染模式,在這個模式下,組件完全在服務(wù)器上運行,讓我們可以組件中做類似查詢數(shù)據(jù)庫的后端操作。

下面是一個“服務(wù)端組件”的簡單示例。

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;

如果你已經(jīng)寫了很多年的 React,這樣的代碼一定會讓你感覺奇怪 ??。

我就是其中之一。當我看到這種寫法時,本能地驚嘆道。 “函數(shù)組件不能異步呀!而且我們不能直接在渲染中出現(xiàn)這樣的副作用!”

這里要理解的關(guān)鍵點是:服務(wù)端組件只會渲染一次,永遠不會重新渲染。它們在服務(wù)器上運行一次生成 UI,并將渲染的值發(fā)送到客戶端并原地鎖定,輸出永遠不會改變。

這表示 React 的 API 的很大一部分與服務(wù)端組件是不兼容的。例如,我們不能使用 useSate(),因為狀態(tài)可以改變,但服務(wù)端組件不支持重新渲染。我們不能使用 useEffect(),因為它只在渲染后在客戶端上運行,而服務(wù)端組件是不會發(fā)送到客戶端的。

不過,由于服務(wù)端環(huán)境限制,也給服務(wù)端組件的編寫帶來一定靈活性。例如:在傳統(tǒng)客戶端 React 中,我們需要將副作用放入 useEffect() 回調(diào)或事件處理程序中,避免每次渲染時重復(fù)調(diào)用。但如果組件本身只運行一次,我們就不必擔心這個問題了!

服務(wù)端組件本身非常簡單,但“React 服務(wù)端組件”模式要復(fù)雜得多。這是因為我們還要支持以前的常規(guī)組件,混用就會帶來混亂。

為了與新的“React 服務(wù)端組件”做區(qū)分,傳統(tǒng) React 組件被稱為“客戶端組件(Client Component)”。老實說,我不是很喜歡這個名字。

“客戶端組件”聽起來好像這些組件只在客戶端上渲染,實際上并非如此——客戶端組件在客戶端和服務(wù)器端都會渲染。

圖片圖片

我知道所有這些術(shù)語都非常令人困惑,所以我做了一下總結(jié):

  • React 服務(wù)端組件(React Server Components)是這個新模式的名稱
  • 我們所了解的“標準”React 組件被重新命名為客戶端組件(Client Component),這是對舊事物的一個新稱呼
  • 這個新模式引入了一個新的類型組件:服務(wù)端組件(Server Component),這些組件專門在服務(wù)器上渲染,其代碼也不會包含在發(fā)送給客戶端的 JS Bundle 中,因此也不會參與水合或重新渲染

?? 服務(wù)端組件與服務(wù)器端渲染

這里必須要澄清一下:React 服務(wù)端組件并不是服務(wù)器端渲染的替代品。你不應(yīng)該把 React Server Components 理解成“SSR 的 2.0 版本”

這 2 者更像是可以拼湊在一起的拼圖,相輔相成。

我們?nèi)匀恍枰?wù)器端渲染來生成初始 HTML。React Server Components 則是建立在基礎(chǔ)之上,讓我們從客戶端 JavaScript 包中省略這些組件,確保它們只在服務(wù)器上運行。

事實上,你也可以在沒有服務(wù)器端渲染的情況下使用 React 服務(wù)端組件。實踐中它們通常一起使用,來得到更好的結(jié)果。如果你想查看示例,React 團隊已經(jīng)構(gòu)建了一個沒有 SSR 的最小 RSC demo[2]。

在使用服務(wù)端組件之前

通常,當新的 React 功能出現(xiàn)時,我們可以通過將 React 依賴項升級到最新版本來使用,類似 npm install react@latest 就可以了,不過服務(wù)端組件不是這樣。

我的理解是:服務(wù)端組件需要與 React 之外的一些系統(tǒng)緊密配合才能使用,比如打包工具(bundler)、服務(wù)器、路由之類的。

當我寫這篇文章時,Next.js 13.4+ 通過引入全新的重新架構(gòu)“App Router” 來支持服務(wù)端組件的使用。

當然,在可以遇見的將來,會有越來越多的基于 React 的框架會支持這一特性。React 官方文檔有一個 “Bleeding-edge frameworks”[3] 的部分,其中列出了支持 React 服務(wù)端組件的框架列表。

使用客戶端組件

在 Next.js App Router 架構(gòu)下,默認所有組件都會被看作服務(wù)端組件,客戶端組件需要特別聲明,這需要通過一個新的指令說明。

'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 這是一個客戶端組件,應(yīng)該包含在 JS Bundle 中,以便在客戶端上重新渲染。

這種聲明方式借鑒了 JavaScript 的嚴格模式聲明——'use strict'。

在 App Router 架構(gòu)下,所有組件默認被看作是服務(wù)端組件,無需任何聲明。當然,你可能會想到服務(wù)端組件是不是使用 'use server'——NO,不是!'use server' 其實是用在 Server Actions,而非服務(wù)端組件上的,不過這塊內(nèi)容超出了本文范圍就不講了,有興趣的同學(xué)可以私下學(xué)習(xí)。

?? 哪些組件應(yīng)該是客戶端組件?

這里你可能就有疑問了:我該怎么知道一個組件應(yīng)該是服務(wù)端組件還是客戶端組件呢?

這里可以給大家一個一般規(guī)則:如果一個組件可以是服務(wù)端組件,那么它就應(yīng)該是服務(wù)端組件。服務(wù)端組件往往更簡單且更容易推理,還有一個性能優(yōu)勢,即服務(wù)端組件不在客戶端上運行,所以它們的代碼不包含在我們的 JavaScript 包中。因此,React 服務(wù)端組件對改進頁面交互指標(TTI)有所幫助。

不過,這不意味著我們要盡可能把作為組件都改成服務(wù)端組件,不合理也不可能。在 RSC 之前,每個 React 應(yīng)用程序中的 React 組件都是客戶端組件。

當你開始使用 React 服務(wù)端組件時,你會發(fā)現(xiàn)它寫起來這非常直觀。而我們的一些組件由于需要狀態(tài)或 Effect,只能在客戶端上運行。你可以通過在組件頂部添加 'use client' 指令指定當前組件是客戶端組件,否則默認就是服務(wù)端組件。

客戶端邊界

當我熟悉 React 服務(wù)端組件時,我遇到的第一個問題是:如果組建 props 改變了,會發(fā)生什么?

假設(shè),我們有一個像這樣的服務(wù)端組件:

function HitCounter({ hits }) {
  return (
    <div>
      Number of hits: {hits}
    </div>
  );
}

如果在初始服務(wù)器端渲染中, hits 等于 0 。然后,這個組件將生成以下結(jié)果。

<div>
  Number of hits: 0
</div>

但是,如果 hits 的值發(fā)生變化會怎樣?假設(shè)它是一個狀態(tài)變量,從 0 更成了 1。HitCounter 這個時候就需要重新渲染,但它不能重新渲染,因為它是服務(wù)端組件!

這里的問題是,如果沒有上下文環(huán)境,只是孤立的考慮服務(wù)端組件并沒有真正的意義。我們必須擴大范圍,從更高的角度審視,考慮我們應(yīng)用程序的結(jié)構(gòu)。

假設(shè)我們有如下的組件樹結(jié)構(gòu):

圖片圖片

如果所有這些組件都是服務(wù)端組件,那么就不會存在上面的問題,因為所有組件都不會重新渲染,props 也就沒有改變的可能性。

但假設(shè) Article 組件擁有 hits 狀態(tài)變量。為了使用狀態(tài),我們需要將其轉(zhuǎn)換為客戶端組件:

圖片圖片

你觀察到這里的問題了嗎?當 Article 重新渲染時,任何下屬子組件也會重新渲染,包括 HitCounter 和 Discussion。但是,如果這些是服務(wù)端組件,是無法重新渲染的。

為了避免這類矛盾場景的出現(xiàn),React 團隊添加了一條規(guī)則:客戶端組件只能導(dǎo)入其他客戶端組件。'use client' 指令表示 HitCounter 和 Discussion 的這些實例將自動成為客戶端組件。

我在使用 React 服務(wù)端組件時遇到的最大的“啊哈(ah-ha)”時刻之一,是意識到服務(wù)端組件的這種新模式其實就是關(guān)于創(chuàng)建客戶端邊界的(client boundaries)。在實踐中,總會遇到下面的場景:

圖片圖片

當我們將 'use client' 指令添加到 Article 組件時,我們創(chuàng)建了一個“客戶端邊界”。邊界內(nèi)的所有組件都隱式成為客戶端組件。即使像 HitCounter 這樣的組件沒有使用 'use client' 指令,在這種特殊情況下它們?nèi)匀粫诳蛻舳松线M行水合和渲染。

也就是說,我們不必將 'use client' 添加到每個客戶端上運行的組件,只需要在創(chuàng)建新的客戶端邊界的組件上添加即可。

解決服務(wù)端組件帶來的限制問題

當我第一次了解到客戶端組件無法渲染服務(wù)端組件時,它對我來說感覺非常限制。如果我需要在應(yīng)用程序中使用高層狀態(tài)怎么辦?那所有組件豈不是都成為客戶端組件了?

事實證明,在許多情況下,我們可以通過重構(gòu)組件來解決這個限制。

這是一件很難解釋的事情,所以讓我們先舉個例子說明:

'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 狀態(tài)允許用戶在深色/淺色模式之間切換。這類功能通常需要在應(yīng)用程序樹的較高層級設(shè)置,以便我們可以將 CSS 變量 token 應(yīng)用到 <body> 上。

為了使用狀態(tài),我們需要讓 Homepage 成為客戶端組件。由于這是我們應(yīng)用程序的頂部,表示其他所有組件 - 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>
  );
}

現(xiàn)在就可以從 Homepage 中刪除 'use client' 指令了,因為它不再使用狀態(tài)或任何其他客戶端 React 功能,也就表示 Header 和 MainContent 不再需要被迫轉(zhuǎn)換成客戶端組件了!

當然,你可能會有疑問了。ColorProvider 是一個客戶端組件,是 Header 和 MainContent 的父組件。不管怎樣,它仍然處在樹結(jié)構(gòu)的較高層級,是吧?

確實。不過,Header 和 MainContent 是在 Homepage 中引入的,這表示它們的 props 只受到 HomaPage 影響。也就是說,客戶端邊界只對邊界頂部組件的內(nèi)部有影響,對同處于一個父組件下的其他組件沒有影響。

請記住,我們試圖解決的問題是服務(wù)端組件無法重新渲染的問題,因此無法為它們的任何子組件設(shè)置新的 props。Homepage 決定 Header 和 MainContent 的 props 是什么,并且由于 Homepage 本身是一個服務(wù)端組件,那么同屬于服務(wù)端組件的 Header、MainContent 自然就沒有 props 會改變的擔憂。

不得不承認的是,理解服務(wù)端組件架構(gòu)確實是一件費腦筋的事情。即使有了多年的 React 經(jīng)驗,我仍然覺得這很令人困惑,需要相當多的練習(xí)才能培養(yǎng)對這種新架構(gòu)的直覺。

更準確地說,'use client' 指令是在文件/模塊級別下工作的??蛻舳私M件中導(dǎo)入的任何模塊也必須是客戶端組件。畢竟,當打包工具打包我們的代碼時,也是依據(jù)這些導(dǎo)入聲明一同打包的!

淺析底層實現(xiàn)

現(xiàn)在讓我們從一個較低的層面來看服務(wù)端組件的實現(xiàn)。當我們使用服務(wù)端組件時,輸出是什么樣的?實際生成了什么?

讓我們從一個超級簡單的 React 應(yīng)用程序開始:

function Homepage() {
  return (
    <p>
      Hello world!
    </p>
  );
}

在 Next.js App Router 模式下,所有組件默認都是服務(wù)端組件。也就是說,Homepage 就是服務(wù)端組件,會在服務(wù)端渲染。

當我們在瀏覽器中訪問此應(yīng)用程序時,我們將收到一個 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 應(yīng)用程序生成的 UI,即“Hello world!”段落。其實這屬于服務(wù)器端渲染結(jié)果,跟 React 服務(wù)端組件沒有關(guān)系。

再往下,是一個 <script> 標簽來加載我們的 JS 包。這個腳本中包括 React 等依賴項,以及我們應(yīng)用程序中使用的所有客戶端組件代碼。由于我們的 Homepage 是服務(wù)端組件,所以這個組件的代碼不包含在這個 JS 包中。

最后,第二個 <script> 標簽,其中包含一些內(nèi)聯(lián) JS:

self.__next['$Homepage-1'] = {
  type: 'p',
  props: null,
  children: "Hello world!",
};

這里就比較有趣了。本質(zhì)上這里所做的就是告訴 React——“嘿,我知道你看不到 Homepage 組件代碼,但不用擔心:這就是它渲染的內(nèi)容”。通常來說,當 React 在客戶端上水合時,這種做法會加速整個渲染進程,因為部分組件(服務(wù)端組件)已經(jīng)在后端渲染出來了,其組件代碼也不會包含在 JS 文件中。

我們會將服務(wù)器生成的虛擬表示發(fā)送回去,當 React 在客戶端加載時,它會重用這這部分虛擬描述,而不是重新生成它。

這就是上面的 ColorProvider 能夠工作的原因。 Header 和 MainContent 的輸出通過 children 屬性傳遞到 ColorProvider 組件。ColorProvider 可以根據(jù)需要重新渲染,但數(shù)據(jù)是靜態(tài)的,在服務(wù)器就鎖定了。

如果你想了解服務(wù)端組件如何序列化并通過網(wǎng)絡(luò)發(fā)送的,可以使用 Alvar Lagerl?f 開發(fā)的 RSC Devtools[4] 進行查看。

?? 服務(wù)端組件不需要服務(wù)器

我們有一道,服務(wù)器端渲染其實是很多不同渲染策略的總稱。包括:

  1. 靜態(tài)的:HTML 是在構(gòu)建階段生成的
  2. 動態(tài)的:HTML 是在用戶請求是生成的,即“按需”生成的

React Server Components 與上述這 2 渲染策略都是兼容的。當服務(wù)端組件在 Node.js 調(diào)用渲染時,會返回的當前組件的 JavaScript 對象表示。這個操作可以在構(gòu)建時,也可以在請求時。

也就是說,在沒有服務(wù)器的情況下使用 React 服務(wù)端組件!我們可以生成一堆靜態(tài) HTML 文件并將它們托管在某個地方,事實上,這就是 Next.js App Router 中默認就是這個策略——除非我們真的需要推遲到“請求”階段,否則所有這些工作都會在構(gòu)建期間提前發(fā)生。

服務(wù)端組件的好處

React 服務(wù)端組件比較酷的一點就在于:它是 React 中運行服務(wù)器專有代碼的第一個“官方”方案。另外,自 2016 年以來,我們已經(jīng)能夠在 Next.js 的 App Router 模式下使用服務(wù)端組件了!

不過,這種方案引入之后,編寫 React 代碼的方式變得很不一樣了,因為我們需要編寫專用于服務(wù)端的 React 的代碼了。

這樣帶來的一個最明顯好處就是性能了。服務(wù)端組件不包含在我們發(fā)送給客戶端的 JS 包中,這樣就減少了需要下載的 JS 代碼數(shù)量以及需要水合的組件數(shù)量:

圖片圖片

不過,這對我來說可能是最不令人興奮的事情。畢竟,大多數(shù) Next.js 應(yīng)用程序在“頁面可交互(Page Interactive)”方面已經(jīng)做得足夠快了。

如果你遵循語義 HTML 原則,那么你的大部分應(yīng)用程序甚至在 React 水合之前就可以運行。比如:跳轉(zhuǎn)鏈接、提交表單、展開和折疊手風琴(使用 <details> 和 <summary>)等。者對于大多數(shù)項目來說,React 只需要幾秒鐘的時間來進行水合就很不錯了。

不過,React 服務(wù)端組件真正的優(yōu)勢在于,我們不再需要在功能與打包文件尺寸上妥協(xié)了!

例如,大多數(shù)技術(shù)博客都需要某種語法高亮庫。在我的博客里,我使用 Prism。代碼片段如下所示:

function exampleJavaScriptFunction(param) {
  return "Hello world!"
}

一個流行語法高亮庫,通常會支持很多流行的編程語言,有幾兆字節(jié),放到 JS 包中實在太大。因此,我們必須做出妥協(xié),刪除非必須語言和功能。

但是,假設(shè)我們在服務(wù)端組件中進行語法突出顯示。在這種情況下,我們的 JS 包中實際上不會包含高亮庫代碼。因此,我們不必做出任何妥協(xié),另外我們還可以使用所有的附加功能。

Bright[5] 就是支持在服務(wù)端組件中使用的現(xiàn)代語法高亮庫。

圖片圖片

這是讓我對 React 服務(wù)端感到興奮的一個地方。原本包含在 JS 包中成本太高的東西現(xiàn)在可以在服務(wù)器上運行,而不必在包含在 JS 包中了,這也帶來了更好的用戶體驗。

這也不僅僅是性能和用戶體驗。使用 RSC 一段時間后,我開始真正體會到服務(wù)端組件是多么簡單易用。我們永遠不必擔心依賴數(shù)組、過時的閉包、記憶或由事物變化引起的任何其他復(fù)雜的東西。

我真的很高興看到未來幾年事情將如何發(fā)展,因為社區(qū)將利用這種新模式繼續(xù)創(chuàng)造出像 Bright 這樣新的解決方案。對于成為一名 React 開發(fā)者來說,這很令人激動!

完整圖表

React 服務(wù)端組件是一項令人興奮的方案,但它實際上只是“現(xiàn)代 React”難題的一部分。

當我們將 React 服務(wù)端組件與 Suspense 和新的 Streaming SSR 架構(gòu)結(jié)合起來時,事情變得更加有趣。它允許我們做下面這樣瘋狂的事情:

圖片圖片

簡單來說,內(nèi)置 Suspense 組件能夠利用 Streaming SSR + React 服務(wù)端組件架構(gòu)實現(xiàn)局部組件更新。這樣每塊內(nèi)容都可以單獨渲染、處理,能更快響應(yīng)用戶,帶來更好地瀏覽體驗。

不過這部分知識超出了本文范圍,你可以在 Github[6] 上了解有關(guān)此架構(gòu)的更多信息。

參考資料

[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

責任編輯:武曉燕 來源: 寫代碼的寶哥
相關(guān)推薦

2024-02-22 17:54:30

React編譯器團隊

2024-03-28 08:50:58

Flink分配方式后端

2023-09-26 00:53:37

B端搭建低代碼

2024-02-04 09:27:09

ReactNativeJS

2024-01-29 09:01:20

React列表模式

2024-06-07 14:54:55

2021-12-10 07:45:48

字節(jié)音頻視頻

2023-01-04 18:10:26

服務(wù)模塊化jre

2021-10-26 09:55:52

CAP理論分布式

2022-08-29 07:48:27

文件數(shù)據(jù)參數(shù)類型

2025-03-17 11:21:08

APISwagger界面

2023-08-04 08:20:56

DockerfileDocker工具

2021-01-12 05:08:49

DHCP協(xié)議模型

2021-08-27 07:06:09

DubboDocker技術(shù)

2022-03-31 18:59:43

數(shù)據(jù)庫InnoDBMySQL

2022-05-24 08:21:16

數(shù)據(jù)安全API

2023-08-10 08:28:46

網(wǎng)絡(luò)編程通信

2022-10-18 07:33:57

Maven構(gòu)建工具

2023-09-10 21:42:31

2023-06-30 08:18:51

敏捷開發(fā)模式
點贊
收藏

51CTO技術(shù)棧公眾號