截止目前,React Server Component 還在開發與研究中,所以不適合投入生產環境使用。但其概念很是有趣,值得技術人學習。php
目前除了國內各類博客、知乎解讀外,最一手的學習資料有下面兩處:html
我會結合這些一手資料,與一些業界大牛的解讀,系統的講清楚 React Server Component 的概念,以及我對它的一些理解。前端
首先咱們來看,爲何須要提出 Server Component 這個概念:mysql
Server Component 概念的提出,是爲了解決 "用戶體驗、可維護性、性能" 這個不可能三角,所謂不可能三角就是,最多同時知足兩條,而沒法三條都同時知足。react
簡單解釋一下,用戶體驗體如今頁面更快的響應、可維護性體如今代碼應該高內聚低耦合、性能體如今請求速度。webpack
一言蔽之,在先後端解耦的模式下,惟一鏈接的橋樑就是取數請求。要把用戶體驗作好,取數就要提早並行發起,而前端模塊是獨立維護的,因此在前端作取數聚合這件事,必然會破壞前端可維護性,而這並行這件事放在後端的話,會由於後端不能解析前端模塊,致使給出的聚合信息滯後,長此以往變得冗餘。git
要解決這個問題,就必須加深前端與後端的聯繫,因此像 GraphQL 這種先後端約定方案是可行的,但由於其部署成本高,收益又僅在前端,因此難以在後端推廣。github
Server Component 是另外一種方案,經過啓動一個 Node 服務輔助前端,但作的不是 API 對接,而是運行前端同構 js 代碼,直接解析前端渲染模塊,從中自動提取請求並在 Node 端直接與服務器通訊,由於服務端間通訊成本極低、前端代碼又不須要作調整,請求數據也是動態按需聚合的,所以同時解決了 "用戶體驗、可維護性、性能" 這三個問題。web
其核心改進點以下圖所示:sql
<img width=300 src="https://img.alicdn.com/imgextra/i2/O1CN01NttXOI21kaFJgNDx1_!!6000000007023-2-tps-720-466.png">
如上圖所示,這是先後端正常交互模式,能夠看到,Root
與 Child
串行發了兩個請求,由於網絡耗時與串行都是嚴重阻塞部分,所以用紅線標記。
Server Component 能夠理解爲下圖,不只減小了一次網絡損耗,請求也變成了並行,請求返回結果也從純數據變成了一個同時描述 UI DSL 與數據的特殊結構:
<img width=500 src="https://img.alicdn.com/imgextra/i1/O1CN01MDYxZ71K0IkACLmFJ_!!6000000001101-2-tps-1142-468.png">
到此,恭喜你已經理解了 Server Component 核心概念,若是你只想泛泛瞭解一下,讀到這裏就能夠結束了。若是你還想深刻了解其實現細節,請繼續閱讀。
歸納的說,Server Component 就是讓組件擁有在服務端渲染的能力,從而解決不可能三角問題。也正由於這個特性,使得 Server Component 擁有幾種讓人眼前一亮的特性,都是純客戶端組件所不具有的:
Server Component 將組件分爲三種:Server Component、Client Component、Shared Component,分別以 .server.js
、.client.js
、.js
後綴結尾。
其中 .client.js
與普通組件同樣,但 .server.js
與 .js
均可能在服務端運行,其中:
.server.js
必然在服務端執行。.js
在哪執行要看誰調用它,若是是 .server.js
調用則在服務端執行,若是是 .client.js
調用則在客戶端執行,所以其本質還要接收服務端組件的約束。下面是 RFC 中展現的 Server Component 例子:
// Note.server.js - Server Component import db from 'db.server'; // (A1) We import from NoteEditor.client.js - a Client Component. import NoteEditor from 'NoteEditor.client'; function Note(props) { const {id, isEditing} = props; // (B) Can directly access server data sources during render, e.g. databases const note = db.posts.get(id); return ( <div> <h1>{note.title}</h1> <section>{note.body}</section> {/* (A2) Dynamically render the editor only if necessary */} {isEditing ? <NoteEditor note={note} /> : null } </div> ); }
能夠看到,這就是 Node 與 React 混合語法。服務端組件有着苛刻的限制條件:不能有狀態,且 props
必須能被序列化。
很容易理解,由於服務端組件要被傳輸到客戶端,就必須通過序列化、反序列化的過程,JSX 是能夠被序列化的,props 也必須遵循這個規則。另外服務端不能幫客戶端存儲狀態,所以服務端組件不能用任何 useState
等狀態相關 API。
但這兩個問題均可以繞過去,即將狀態轉化爲組件的 props
入參,由 .client.js
存儲,見下圖:
<img width=250 src="https://img.alicdn.com/imgextra/i4/O1CN01ChPZdO1ky0Nsu2ygV_!!6000000004751-2-tps-514-278.png">
或者利用 Server Component 與 Client Component 無縫集成的能力,將狀態與沒法序列化的 props
參數都放在 Client Component,由 Server Component 調用。
這句話聽起來有點誇張,但其實在 Server Component 限定條件下還真的是。看下面代碼:
// NoteWithMarkdown.js import marked from 'marked'; // 35.9K (11.2K gzipped) import sanitizeHtml from 'sanitize-html'; // 206K (63.3K gzipped) function NoteWithMarkdown({text}) { const html = sanitizeHtml(marked(text)); return (/* render */); }
marked
與 sanitize-html
都不會被下載到本地,因此若是隻有這一個文件傳輸,客戶端的理論增長體積就是 render
函數序列化後字符串大小,可能不到 1KB。
固然這背後也是限制換來的,首先這個組件沒有狀態,沒法在客戶端實時執行,並且在服務端運行也可能消耗額外計算資源,若是某些 npm 包計算複雜度較高的話。
這個好處能夠理解爲,marked
這個包僅在服務端讀取到內存一次,之後只要後客戶端想用,只須要在服務端執行 marked
API 並把輸出結果返回給客戶端,而不須要客戶端下載 marked
這個包了。
因爲 Server Component 在服務端執行,所以能夠執行 Nodejs 的任何代碼。
// Note.server.js - Server Component import fs from 'react-fs'; function Note({id}) { const note = JSON.parse(fs.readFile(`${id}.json`)); return <NoteWithMarkdown note={note} />; }
咱們能夠把對請求的理解拔高一個層次,即 request
只是客戶端發起的一個 Http 請求,其本質是訪問一個資源,在服務端就是個 IO 行爲。對於 IO,咱們還能夠經過 file
文件系統寫入刪除資源、db
經過 sql 語法直接訪問數據庫,或者 request
直接在服務器本地發出請求。
咱們都知道 webpack 能夠經過靜態分析,將沒有使用到的 import 移出打包,而 Server Component 能夠在運行時動態分析,將當前分支邏輯下沒有用到的 import 移出打包:
// PhotoRenderer.js import React from 'react'; // one of these will start loading *once rendered and streamed to the client*: import OldPhotoRenderer from './OldPhotoRenderer.client.js'; import NewPhotoRenderer from './NewPhotoRenderer.client.js'; function Photo(props) { // Switch on feature flags, logged in/out, type of content, etc: if (props.useNewPhotoRenderer) { return <NewPhotoRenderer {...props} />; } else { return <OldPhotoRenderer {...props} />; } }
這是由於 Server Component 構建時會進行預打包,運行時就是一個動態的包分發器,徹底能夠經過當前運行狀態好比 props.xxx
來區分當前運行到哪些分支邏輯,而沒有運行到哪些分支邏輯,而且僅告訴客戶端拉取當前運行到的分支邏輯的缺失包。
純前端模式與之相似的寫法是:
const OldPhotoRenderer = React.lazy(() => import('./OldPhotoRenderer.js')); const NewPhotoRenderer = React.lazy(() => import('./NewPhotoRenderer.js'));
只是這種寫法不夠原生,且實際場景每每只有前端框架把路由自動包一層 Lazy Load,而普通代碼裏不多出現這種寫法。
通常考慮到取數網絡消耗,咱們每每會將其處理成異步,而後在數據返回前展現 Loading:
// Note.js function Note(props) { const [note, setNote] = useState(null); useEffect(() => { // NOTE: loads *after* rendering, triggering waterfalls in children fetchNote(props.id).then(noteData => { setNote(noteData); }); }, [props.id]); if (note == null) { return "Loading"; } else { return (/* render note here... */); } }
這是由於單頁模式下,咱們能夠快速從 CDN 拿到這個 DOM 結構,但若是再等待取數,總體渲染就變慢了。而 Server Component 由於自己就在服務端執行,所以能夠將拿 DOM 結構與取數同時進行:
// Note.server.js - Server Component function Note(props) { // NOTE: loads *during* render, w low-latency data access on the server const note = db.notes.get(props.id); if (note == null) { // handle missing note } return (/* render note here... */); }
固然這個前提是網絡消耗敏感的狀況,若是自己就是一個慢 SQL 查詢,耗時幾秒的狀況下,這樣作反而拔苗助長。
看下面的例子:
// Note.server.js // ...imports... function Note({id}) { const note = db.notes.get(id); return <NoteWithMarkdown note={note} />; } // NoteWithMarkdown.server.js // ...imports... function NoteWithMarkdown({note}) { const html = sanitizeHtml(marked(note.text)); return <div ... />; } // client sees: <div> <!-- markdown output here --> </div>
雖然在組件層面抽象了 Note
與 NoteWithMarkdown
兩個組件,但因爲真正 DOM 內容實體只有一個簡單的 div
,因此在 Server Component 模式下,返回內容就會簡化爲這個 div
,而無需包含那兩個抽象的組件。
Server Component 模式下有三種組件,分別是 Server Component、Client Component、Shared Component,其各自都有一些使用限制,以下:
Server Component:
useState
、useReducer
等狀態存儲 API。useEffect
等生命週期 API。window
等僅瀏覽器支持的 API。Client Component:
❌ 不能引用 Server Component。
<ClientTabBar><ServerTabContent /></ClientTabBar>
。Shared Component:
useState
、useReducer
等狀態存儲 API。useEffect
等生命週期 API。window
等僅瀏覽器支持的 API。其實不難理解,由於 Shared Component 同時在服務器與客戶端使用,所以兼具它們的劣勢,帶來的好處就是更強的複用性。
要快速理解 Server Component,我以爲最好也是最快的方式,就是找到其與十年前 PHP + HTML 的區別。看下面代碼:
$link = mysqli_connect('localhost', 'root', 'root'); mysql_select_db('test', $link); $result = mysql_query('select * from table'); while($row=mysql_fetch_assoc($result)){ echo "<span>".$row["id"]."</span>"; }
其實 PHP 早就是一套 "Server Component" 方案了,在服務端直接訪問 DB、並返回給客戶端 DOM 片斷。
React Server Component 在折騰了這麼久後,能夠發現,最大的區別是將返回的 HTML 片斷改成了 DSL 結構,這實際上是瀏覽器端有一個強大的 React 框架在背後撐腰的結果。而這個帶來的好處除了可讓咱們在服務端能繼續寫 React 語法,而不用退化到 "PHP 語法" 之外,更重要的是組件狀態得以維持。
另外一個重要不一樣是,PHP 沒法解析如今前端生態下任何 npm 包,因此無從解析模塊化的前端代碼,因此雖然直覺上感受 PHP 效率與 Server Component 並沒有區別,但背後的成本是得寫另外一套不依賴任何 npm 包、JSX 的語法來返回 HTML 片斷,Server Component 大部分特性都沒法享受到,並且代碼也沒法複用。
因此,本質上仍是 HTML 太簡單了,沒法適應現在前端的複雜度,而普通後端框架雖而後端能力強大,但在前端能力上還停留在 20 年前(直接返回 DOM),惟有 Node 中間層方案做爲橋樑,才能較好的銜接現代後端代碼與現代前端代碼。
其實在 PHP 時代,先後端均可以作模塊化。後端模塊化顯而易見,由於能夠將後端代碼模塊化的開發,最後打包至服務器運行。前端也能夠在服務端模塊化開發,只要咱們將先後端代碼剝離出來便可,下圖青色是後端部分,紅色是前端部分:
<img width=400 src="https://img.alicdn.com/imgextra/i3/O1CN01jsKjLq1iWPHi9C4pQ_!!6000000004420-2-tps-894-642.png">
但這有個問題,由於後端服務對瀏覽器來講是無狀態的,因此後端模塊化自己就符合其功能特徵,但前端頁面顯示在用戶瀏覽器,每次都經過路由跳轉到新頁面,顯然不能最大程度發揮客戶端持續運行的優點,咱們但願在保持前端模塊化的基礎上,在瀏覽器端有一個持續運行的框架優化用戶體驗,所以 Server Component 其實作了下面的事情:
<img width=550 src="https://img.alicdn.com/imgextra/i3/O1CN01gzaZNY1lBkGbGJKUy_!!6000000004781-2-tps-1332-760.png">
這樣作有兩大好處:
Server Component 尚未成熟,但其理念仍是很靠譜的。
想要同時實現 "用戶體驗、可維護性、性能",重後端,或者重前端的方案都不可行,只有在先後端取得一種平衡才能達到。Server Component 表達了一種職業發展理念,即將來先後端仍是會走向全棧,這種全棧是先後端同時作深,從而讓程序開發達到純前端或純後端沒法達到的高度。
2021 年國內開發環境依然比較落後,所謂全棧,每每指的是 「先後端都懂一點」,各端都作不深,難以孵化出 Server Component 這種概念。固然,這也是咱們繼續向世界學習的動力。
也許 PHP 與 Server Component 的區別,就是檢驗一我的是真全棧仍是僞全棧的試金石,快去問問你的同事吧!
討論地址是: 精讀《React Server Component》· Issue #311 · dt-fe/weekly
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證)