banner
AgedCoffee

AgedCoffee

重新思考React最佳實踐

重新思考 React 最佳實踐#

原文地址

十多年前,React 重新思考了客戶端渲染單頁應用程序的最佳實踐。

如今,React 的採用率達到了頂峰,並繼續受到健康的批評和質疑。

React 18 和 React 伺服器組件(RSCs)一起,標誌著從最初的 “視圖” 客戶端 MVC 的標籤線,到一個重要的階段轉變。

在這篇文章中,我們將試圖理解 React 從 React 庫到 React 架構的演變。

安娜・卡列尼娜原則指出:“所有幸福的家庭都是相似的,而每個不幸的家庭都以自己的方式不幸。”

我們將從了解 React 的核心約束和過去管理它們的方法開始。探討團結幸福 React 應用程序的基本模式和原則。

到最後,我們將了解 React 框架(如 Remix 和 Next 13 應用程序目錄)中不斷變化的心智模型。

讓我們從了解到目前為止一直試圖解決的潛在問題開始。這將幫助我們將 React 核心團隊的建議置於上下文中,即利用具有伺服器、客戶端和打包程序之間緊密集成的高級框架。

正在解決什麼問題?#

在軟件工程中,通常有兩類問題:技術問題和人際問題。

可以將架構看作是一種隨著時間推移找到合適約束以解決這些問題的過程。

如果沒有解決人際問題的正確約束,那麼人們合作越多,隨著時間的推移,變更的複雜性、容錯性和風險性就越大。如果沒有用於管理技術問題的正確約束,那麼你發布的內容越多,最終用戶體驗通常就越差。

這些約束最終幫助我們管理作為在複雜系統中構建和互動的人類所面臨的最大限制 —— 有限的時間和注意力。

React 和人際問題#

解決人際問題具有高槓桿作用。我們可以在有限的時間和關注力下提高個人、團隊和組織的生產力。

團隊的時間和資源有限,要快速交付。作為個人,我們的大腦容量有限,無法容納大量的複雜性。

我們大部分時間都在弄清楚現狀,以及如何最好地進行改變或添加新內容。人們需要能夠在不將整個系統完全裝載到頭腦中的情況下進行操作。

React 的成功很大程度上歸功於它與當時現有解決方案相比在管理這一約束方面的表現。它允許團隊分頭並行構建解耦組件,這些組件可以聲明式地組合在一起,並通過單向數據流 “順利工作”。

它的組件模型和逃生艙口允許在清晰的邊界內將遺留系統和集成的混亂抽象出來。然而,這種解耦和組件模型的一個影響是,很容易因為樹木而忽視森林的大局。

React 和技術問題#

與當時的現有解決方案相比,React 還簡化了實現複雜交互功能的過程。

它的聲明式模型產生了一個 n-ary 樹數據結構,該結構被輸入到像 react-dom 這樣的特定平台的渲染器中。隨著我們擴大團隊並尋求現成的軟件包,這個樹結構很快就變得非常深入。

自 2016 年重寫以來,React 積極解決了在終端用戶硬件上處理大型、深度樹的技術問題。

在線上,在屏幕的另一邊,用戶的時間和注意力也是有限的。期望值在上升,而注意力跨度在縮短。用戶不關心框架、渲染架構或狀態管理。他們希望無摩擦地完成需要完成的任務。另一個約束是要快速且不讓用戶思考。

我們將看到,下一代 React(以及 React 風格)框架中推薦的許多最佳實踐都緩解了純粹在客戶端 CPU 上處理深度樹所帶來的影響。

回顧偉大的鴻溝#

到目前為止,科技行業在不同軸線上充滿了擺動,比如服務的集中化與去中心化以及薄客戶端與厚客戶端。

我們從厚實的桌面客戶端擺動到隨著 Web 的崛起變得越來越薄,再回到隨著移動計算和 SPA 的崛起變得更厚的客戶端。如今,React 的主導心智模型植根於這種厚實的客戶端方法。

這種轉變在 “前端的前端” 開發人員(擅長 CSS、交互設計、HTML 和可訪問性模式)和 “前端的後端” 之間產生了分歧,因為我們在前後端分離過程中遷移到了客戶端。

在 React 生態系統中,隨著我們試圖調和這兩個世界的最佳實踐,擺動的方向正在回到某個中間地帶,其中很多 “前端的後端” 風格的代碼又被移到了伺服器上。

從 “MVC 中的視圖” 到應用程序架構#

在大型組織中,有一定比例的工程師作為一個平台的一部分,將架構最佳實踐融入其中。

這些開發者使得其他人能夠將有限的時間和精力投入到帶來實際收益的事物上。

受限於有限的時間和注意力帶來的一個影響是,我們通常會選擇感覺最容易的方法。因此,我們希望這些積極的約束能使我們走在正確的道路上,並輕鬆地跌入成功的陷阱

這個成功的重要部分在於減少需要在終端用戶設備上加載和運行的代碼量。遵循只下載和運行必要內容的原則。當我們局限於僅客戶端的範例時,這很難遵守。包最終會包含數據獲取、處理和格式化庫(例如,moment.js),而這些庫可以離開主線程運行。

這在像 Remix 和 Next 這樣的框架中正在發生轉變,React 的單向數據流擴展到了伺服器,其中 MPA 的簡單請求 - 響應心智模型與 SPA 的交互性相結合。

重返伺服器之旅#

現在讓我們了解隨著時間的推移,我們在這個僅客戶端範例上應用了哪些優化。這需要重新引入伺服器以獲得更好的性能。這個背景將幫助我們理解 React 框架,其中伺服器演變成為一等公民。

以下是為客戶端渲染的前端提供服務的簡單方法 - 帶有許多 script 標籤的空白 HTML 頁面:
OZkfAt
圖示顯示了客戶端渲染的基本原理。
這種方法的優點是快速的 TTFB(首字節時間),簡單的操作模型和解耦的後端。與 React 的編程模型結合,這種組合簡化了許多人際問題。

但是我們很快就會遇到技術問題,因為所有的責任都交給了用戶硬件。我們必須等到所有內容都下載並運行,然後從客戶端獲取,才能顯示有用的內容。

隨著代碼積累,只有一個地方可以存放代碼。如果沒有謹慎的性能管理,這可能導致應用程序運行緩慢到令人無法忍受的程度。

進入伺服器端渲染#

我們重返伺服器的第一步是試圖解決這些緩慢的啟動時間。

與其用空白的 HTML 頁面響應初始文檔請求,我們在伺服器上立即開始獲取數據,然後將組件樹渲染為 HTML 並響應。

在客戶端渲染的 SPA 上下文中,SSR 就像是一個技巧,可以在加載 Javascript 時首先顯示一些內容,而不是一片空白的白屏。
rFtSEE
圖示展示了伺服器端渲染和客戶端 hydration 的基本原理。
SSR 可以提高感知性能,尤其是對於內容豐富的頁面。但它帶來了操作成本,對於高度交互性的頁面可能降低用戶體驗 —— 因為 TTI(可交互時間)被進一步推遲。

這被稱為 “不可思議的山谷”,用戶在頁面上看到內容並嘗試與其互動,但主線程被鎖定。問題仍然是過多的 Javascript。

對速度的需求 - 更多優化#

因此,SSR 可以加快速度,但並非靈丹妙藥。

還有一個固有的低效之處,即在伺服器上渲染後,需要在客戶端的 React 接管時重新執行所有操作。

較慢的 TTFB 意味著瀏覽器在請求文檔後必須耐心等待,以便接收頭部信息,從而知道需要下載哪些資源。

這就是流式傳輸發揮作用的地方,它為畫面帶來更多的並行性。

我們可以想像,如果 ChatGPT 在整個回覆完成之前一直顯示旋轉器,大多數人會認為它已經損壞並關閉標籤頁。因此,我們儘早顯示我們能顯示的內容,通過在數據和內容完成時將其流式傳輸到瀏覽器。

對於動態頁面的流式傳輸,是一種儘早在伺服器上開始獲取數據的方法,同時讓瀏覽器開始下載資源,所有這些都是並行進行的。這比上面的圖示快得多,在那裡我們等待所有內容都被獲取和渲染完畢,然後將帶有數據的 HTML 發送給客戶端。

關於流式傳輸的更多信息

這種流式傳輸技術取決於後端伺服器堆棧或邊緣運行時是否能夠支持流式傳輸數據。

對於 HTTP/2,使用 HTTP 流(一種允許並發發送多個請求和響應的功能),而對於 HTTP/1,使用 Transfer-Encoding: chunked 機制,該機制允許將數據分成較小的、獨立的塊進行發送。

現代瀏覽器內置了 Fetch API,可以將獲取到的響應作為可讀流進行消費。

響應的 body 屬性是一個可讀流,允許客戶端在伺服器提供時逐塊接收數據,而不是等待所有塊一次性下載完成。

這種方法需要在伺服器上設置發送流式響應的能力,並在客戶端上進行讀取,這需要客戶端和伺服器之間的密切協作。

流式傳輸還有一些值得注意的細微差別,例如緩存考慮因素、處理 HTTP 狀態碼和錯誤以及實際終端用戶體驗。在這裡,快速 TTFB 與佈局轉換之間存在權衡。

到目前為止,我們已經優化了客戶端渲染樹的啟動時間,通過在伺服器上儘早獲取數據,同時儘早刷新 HTML 以並行下載數據和資源。

現在讓我們關注獲取和變更數據。

數據獲取約束#

分層組件樹的一個約束是,“一切都是組件”,這意味著節點通常具有多種職責,如發起獲取操作、管理加載狀態、響應事件和渲染。

這通常意味著我們需要遍歷樹才能知道要獲取什麼。

在早期,通過 SSR 生成初始 HTML 通常意味著在伺服器上手動遍歷樹。這涉及深入到 React 內部以收集所有數據依賴關係,並在遍歷樹時按順序獲取。

在客戶端,這種 “先渲染再獲取” 的順序導致加載指示器和佈局變動並存。因為遍歷樹會導致連續的網絡瀑布效果。

因此,我們需要一種方法能夠並行獲取數據和代碼,而無需每次都從上到下遍歷樹才能知道要下載什麼。

理解 Relay#

了解 Relay 背後的原理以及它如何應對 Facebook 規模上的挑戰是非常有用的。這些概念將幫助我們理解稍後將看到的模式。

  • 組件具有並置的數據依賴

    在 Relay 中,組件以 GraphQL 片段的形式聲明式地定義它們的數據依賴關係。

    與類似 React Query 這樣也具有並置特性的庫的主要區別是,組件不發起獲取操作。

  • 樹遍歷發生在構建時

    Relay 編譯器遍歷樹,收集每個組件的數據需求並生成一個優化的 GraphQL 查詢。

    通常,這個查詢在運行時的路由邊界(或特定的入口點)執行,允許組件代碼和數據儘早並行加載。

並置支持目標的最有價值的架構原則之一 - 能夠刪除代碼。通過刪除組件,它的數據要求也被刪除,查詢將不再包含它們。

Relay 減輕了處理大型樹形數據結構獲取資源時伴隨的許多權衡。

然而,它可能很複雜,需要 GraphQL,一個客戶端運行時環境,以及一個高級編譯器來在保持高性能的同時協調 DX 屬性。

稍後我們將看到 React Server Components 如何為更廣泛的生態系統遵循類似的模式。

下個最好的選擇#

在獲取數據和代碼時,如何避免遍歷樹,而不採用所有這些方法呢?

這就是像 Remix 和 Next 這樣的框架中伺服器上的嵌套路由發揮作用的地方。

組件的初始數據依賴關係通常可以映射到 URL。其中,URL 的嵌套段映射到組件子樹。這種映射使框架能夠提前識別特定 URL 所需的數據和組件代碼。

例如,在 Remix 中,子樹可以自包含其自己的數據需求,獨立於父路由,編譯器確保嵌套路由並行加載。

這種封裝還通過為獨立子路由提供單獨的錯誤邊界來實現優雅降級。它還允許框架通過查看 URL 來提前預加載數據和代碼,以實現更快的 SPA 轉換。

更多的並行化#

讓我們深入了解 Suspense、concurrent mode 和 streaming 如何增強我們一直在探討的數據獲取模式。

Suspense 允許當數據不可用時,子樹回退到顯示加載界面,並在數據準備好時恢復渲染。

這是一種原語,讓我們能夠在本來同步的樹中聲明性地表示異步性。這使得我們可以在獲取資源和渲染的同時實現並行。

正如我們在 streaming 中看到的,我們可以在不等待所有內容完成渲染之前就儘早地開始發送數據。

在 Remix 中,這種模式通過在路由級數據加載器中使用 defer 函數來表達:

// Remix APIs encourage fetching data at route boundaries
// where nested loaders load in parallel
export function loader ({ params }) {
	// not critical, start fetching, but don't block rendering
	const productReviewsPromise = fetchReview(params.id)
	// critical to display page with this data - so we await
	const product = await fetchProduct(params.id)

	return defer({ product, productReviewsPromise })
}

export default function ProductPage() {
	const { product, productReviewsPromise }  = useLoaderData()
	return (
		<>
			<ProductView product={product}>
			<Suspense fallback={<LoadingSkeleton />}>
				<Async resolve={productReviewsPromise}>
					{reviews => <ReviewsView reviews={reviews} />}
				</Async>
			</Suspense>
		</>
	)
}

在 Next 中,RSC(React Server Components)提供了類似的數據獲取模式,通過在伺服器上使用異步組件來等待關鍵數據。

// Example of similar pattern in a server component
export default async function Product({ id }) {
	// non critical - start fetching but don't block
	const productReviewsPromise = fetchReview(id)
	// critical - block rendering with await
	const product = await fetchProduct(id)
	return (
		<>
			<ProductView product={product}>
			<Suspense fallback={<LoadingSkeleton />}>
				{/* Unwrap promise inside with use() hook */}
				<ReviewsView data={productReviewsPromise} />
			</Suspense>
		</>
	)
}

這裡的原則是儘早在伺服器上獲取數據。理想情況下,通過將加載器和 RSC 靠近數據源來實現。

為了避免不必要的等待,我們對不太關鍵的數據進行流式傳輸,使頁面能夠逐步分階段加載 - 這在 Suspense 中變得很簡單。

值得注意的是,RSC 本身並沒有內置的 API 來支持在路由邊界處獲取數據。如果不小心構建,這可能導致連續的網絡瀑布式請求。

這是框架需要在內置最佳實踐和提供更大靈活性以及更多表面用於誤操作之間進行權衡的一條界線。

值得一提的是,當 RSC 部署在靠近數據的地方時,與客戶端瀑布式請求相比,連續瀑布式請求的影響大大減小。

強調這些模式表明,RSC 需要與能夠將 URL 映射到特定組件的路由器進行更高級別的框架集成。

在我們深入了解 RSC 之前,讓我們花一點時間了解這幅圖畫的另一半。

數據變更#

在僅客戶端模式下管理遠程數據的一種常見模式是將其存儲在某種規範化存儲中(例如 Redux 存儲)。

在這個模型中,變更通常會樂觀地更新內存中的客戶端緩存,然後發送網絡請求以更新伺服器上的遠程狀態。

歷史上,手動管理這些內容涉及大量的樣板代碼,並且容易在我們在《React 狀態管理的新浪潮》中討論的所有邊緣情況中出錯。

Hooks 的出現導致了像 Redux RTK 和 React Query 這樣專注於處理這些邊緣情況的工具的出現。

這要求通過網絡傳輸代碼以處理這些問題,其中值通過 React 上下文傳播。除此之外,當遍歷樹時,創建低效的順序 I/O 操作也變得容易。

那麼,當 React 的單向數據流擴展到伺服器時,這種現有模式將如何改變呢?

大量這種 “前端背後” 的樣式代碼實際上轉移到了後端。

下面是一個摘自 Remix 中的數據流的圖片,它展示了框架正在朝著 MPA(多頁面應用程序)架構中的請求 - 響應模型發展的趨勢。

這種轉變是從將所有事物純粹由客戶端處理的模型轉向伺服器發揮更重要作用的模型。

x8Tsvh

你還可以查看《網絡的下一次轉型》以深入了解這種轉變。

這種模式也擴展到了 RSC(React Server Component),我們稍後將介紹實驗性的 “伺服器操作函數”。在這裡,React 的單向數據流延伸到伺服器,採用簡化的請求 - 響應模型和逐步增強的表單。

從客戶端刪除代碼是這種方法的一個好處。但是,主要的好處是簡化數據管理的心智模型,這反過來又簡化了許多現有的客戶端代碼。

理解 React 伺服器組件#

到目前為止,我們已經利用伺服器作為優化純客戶端方法的途徑。現在,我們對 React 的心智模型深深植根於用戶機器上運行的客戶端渲染樹。

RSC(React Server Component)將伺服器引入為一等公民,而不是事後優化。React 發展壯大,後端嵌入到組件樹中,形成了一個強大的外層。

這種架構轉變導致了許多現有關於 React 應用程序是什麼以及如何部署的心智模型的變化。

最明顯的兩個影響是,我們迄今為止討論過的優化數據加載模式的支持,以及自動代碼拆分。

《構建和交付大規模前端》的第二部分中,我們討論了一些大規模關鍵問題,如依賴管理、國際化和優化的 A/B 測試。

當局限於純客戶端環境時,這些問題在大規模上可能難以解決。RSC 以及 React 18 的許多功能,為框架提供了一組基本工具,用於解決許多這些問題。

一個令人困惑的心智模型變化是,客戶端組件可以渲染伺服器組件。

這有助於幫助我們可視化一個帶有 RSC 的組件樹,因為它們沿著樹連接在一起。客戶端組件通過 “孔洞” 連接,提供客戶端交互。

B3808J

將伺服器擴展到組件樹下是非常強大的,因為我們可以避免向下發送不必要的代碼。而且,與用戶硬件不同,我們對伺服器資源有更多的控制權。

樹的根植根於伺服器,樹幹穿越網絡,樹葉被推送到運行在用戶硬件上的客戶端組件上。

這種擴展模型要求我們了解組件樹中的序列化邊界,這些邊界由 'use client' 指令標記。

這也重新強調了掌握組合的重要性,以便讓 RSC 通過客戶端組件中的子組件或插槽渲染到樹的儘可能深的地方。

伺服器操作函數#

隨著我們將前端的部分領域遷移到伺服器,許多創新的想法正在被探討。這些為客戶端與伺服器之間無縫融合的未來提供了一瞥。

如果我們可以在不需要客戶端庫、GraphQL 或擔心運行時低效瀑布的情況下,獲得與組件共同定位的好處呢?

一個伺服器功能的示例可以在 React 風格的元框架 Qwik city 中看到。類似的想法也在 React (Next) 和 Remix 中探討和討論。

Wakuwork 倉庫還為實現 React 伺服器 “操作函數” 提供了用於數據變異的概念驗證。

與任何實驗性方法一樣,有權衡需要考慮。在客戶端 - 伺服器通信方面,有關安全性、錯誤處理、樂觀更新、重試和競態條件的擔憂。正如我們所了解到的,如果沒有框架進行管理,這些問題通常無法解決。

這種探索還強調了實現最佳用戶體驗和最佳開發者體驗往往需要提高複雜性的高級編譯器優化。

結論#

軟件只是幫助人們完成某些事情的工具 - 許多程序員從未理解這一點。把眼睛放在交付的價值上,不要過度關注工具的細節 - 約翰·卡馬克

隨著 React 生態系統超越僅客戶端範例的發展,了解我們下面和上面的抽象是很重要的。

清楚地理解我們操作的基本限制,使我們能夠做出更明智的權衡決策。

隨著每次擺動,我們獲得新知識和經驗來整合到下一輪迭代中。以前方法的優點仍然有效。像往常一樣,這是一個權衡。

偉大之處在於框架越來越多地提供了更多槓桿工具來賦予開發人員為特定情況做出更精細化權衡。在優化用戶體驗與優化開發者體驗相遇,並且簡單模型 MPAs 與富模型 SPAs 在客戶端和伺服器混合中交匯。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。