精讀《React Server Component》

截止目前,React Server Component 還在開發與研究中,所以不適合投入生產環境使用。但其概念很是有趣,值得技術人學習。php

目前除了國內各類博客、知乎解讀外,最一手的學習資料有下面兩處:html

  1. Dan 的 Server Component 介紹視頻
  2. Server Component RFC 草案

我會結合這些一手資料,與一些業界大牛的解讀,系統的講清楚 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">

如上圖所示,這是先後端正常交互模式,能夠看到,RootChild 串行發了兩個請求,由於網絡耗時與串行都是嚴重阻塞部分,所以用紅線標記。

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 擁有幾種讓人眼前一亮的特性,都是純客戶端組件所不具有的:

  • 運行在服務端的組件只會返回 DSL 信息,而不包含其餘任何依賴,所以 Server Component 的全部依賴 npm 包都不會被打包到客戶端。
  • 能夠訪問服務端任何 API,也就是讓組件擁有了 Nodejs 能擁有的能力,你理論上能夠在前端組件裏幹任何服務端才能乾的事情。
  • Server Component 與 Client Component 無縫集成,能夠經過 Server Component 無縫調用 Client Component。
  • Server Component 會按需返回信息,在當前邏輯下,走不到的分支邏輯的全部引用都不會被客戶端引入。好比 Server Component 雖然引用了一個巨大的 npm 包,但某個分支下沒有用到這個包提供的函數,那客戶端也不會下載這個巨大的 npm 包到本地。
  • 因爲返回的不是 HTML,而是一個 DSL,因此服務端組件即使從新拉取,已經產生的 State 也會被維持住。好比說 A 是 ServerComponent,其子元素 B 是 Client Component,此時對 B 組件作了狀態修改好比輸入一些文字,此時觸發 A 從新拉取 DSL 後,B 已經輸入的文字還會保留。
  • 能夠無縫與 Suspense 結合,並不會由於網絡緣由致使連 Suspense 的 loading 都不能及時展現。
  • 共享組件能夠同時在服務端與客戶端運行

三種組件

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

markedsanitize-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 直接在服務器本地發出請求。

運行時 Code Split

咱們都知道 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 查詢,耗時幾秒的狀況下,這樣作反而拔苗助長。

減小 Component 層次

看下面的例子:

// 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>

雖然在組件層面抽象了 NoteNoteWithMarkdown 兩個組件,但因爲真正 DOM 內容實體只有一個簡單的 div,因此在 Server Component 模式下,返回內容就會簡化爲這個 div,而無需包含那兩個抽象的組件。

限制

Server Component 模式下有三種組件,分別是 Server Component、Client Component、Shared Component,其各自都有一些使用限制,以下:

Server Component

  • ❌ 不能用 useStateuseReducer 等狀態存儲 API。
  • ❌ 不能用 useEffect 等生命週期 API。
  • ❌ 不能用 window 等僅瀏覽器支持的 API。
  • ❌ 不能用包含了上面狀況的自定義 Hooks。
  • ✅ 可無縫訪問服務端數據、API。
  • ✅ 可渲染其餘 Server/Client Component

Client Component

  • ❌ 不能引用 Server Component。

    • ✅ 但能夠在 Server Component 中出現 Client Component 調用 Server Component 的狀況,好比 <ClientTabBar><ServerTabContent /></ClientTabBar>
  • ❌ 不能調用服務端 API 獲取數據。
  • ✅ 能夠用一切 React 與瀏覽器完整能力。

Shared Component

  • ❌ 不能用 useStateuseReducer 等狀態存儲 API。
  • ❌ 不能用 useEffect 等生命週期 API。
  • ❌ 不能用 window 等僅瀏覽器支持的 API。
  • ❌ 不能用包含了上面狀況的自定義 Hooks。
  • ❌ 不能引用 Server Component。
  • ❌ 不能調用服務端 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 VS Server Component

其實在 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">

這樣作有兩大好處:

  1. 兼顧了 PHP 模式下優點,即先後端代碼無縫混合,帶來一系列體驗和能力加強。
  2. 先後端仍是各自模塊化編寫,圖中紅色部分是隨前端項目總體打包的,所以開發仍是保留了模塊化特色,且在瀏覽器上還保持了 React 現代框架運行,不管是單頁仍是數據驅動等特性都能繼續使用。

總結

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 許可證
相關文章
相關標籤/搜索