三種不同架構(gòu)中的 React Suspense

React Suspense 的發(fā)展歷程頗為曲折:多年間它幾乎沒有被使用,且被認(rèn)為收效甚微,僅是呈現(xiàn)加載狀態(tài)的一種炫酷方式保檐。然而,隨著 React 18 的推出崔梗,Suspense 提供了一整套新的優(yōu)勢夜只,值得重新審視。遺憾的是蒜魄,這些優(yōu)勢從平淡無奇到較為深奧不等扔亥,并且對應(yīng)用架構(gòu)的依賴性較高爪膊。接下來我們將看看當(dāng)今最常見的三種渲染架構(gòu),以及 React Suspense 在其中所扮演的角色砸王。

簡要概述

注意:
如果將來禁用此彈窗推盛,點擊標(biāo)題將會將鏈接復(fù)制到剪貼板。這是一個很棒的 URL谦铃,當(dāng)頁面加載時它將滾動到對應(yīng)的部分耘成!

關(guān)閉

  • 客戶端渲染:在 React.lazy 加載時顯示回退狀態(tài);使用支持 suspense 的框架以聲明方式處理數(shù)據(jù)加載和錯誤狀態(tài)驹闰。
  • 服務(wù)端渲染:上述功能 + 將服務(wù)端渲染的組件包裹在 <Suspense /> 中瘪菌,以便在客戶端選擇性地進行水合。
  • 服務(wù)端組件:上述功能 + 將異步服務(wù)端組件包裹在 <Suspense /> 中嘹朗,分階段流式傳輸?shù)娇蛻舳耍菏紫仁腔赝藸顟B(tài)师妙,接著是最終內(nèi)容。

深入探討

客戶端渲染

這是最基本的 React 用法屹培∧ǎ客戶端請求時,服務(wù)器返回一個只包含基本 HTML 的文件褪秀,其中包含引用 JavaScript 包的 <script> 標(biāo)簽蓄诽。當(dāng) JavaScript 加載并執(zhí)行后,生成頁面內(nèi)容并填充空白的 HTML 文件媒吗。所有導(dǎo)航完全在客戶端進行仑氛,不再向服務(wù)器發(fā)出額外請求——這引出了 Suspense 的第一個用例。由于 JavaScript 包含了生成應(yīng)用任何部分所需的代碼闸英,因此文件體積可能較大锯岖。頁面內(nèi)容在渲染之前,必須加載甫何、解析并執(zhí)行整個 JavaScript 文件出吹,這成為一個嚴(yán)重的性能瓶頸。不過沛豌,你不需要在每個頁面加載所有代碼趋箩。我們可以將應(yīng)用拆分成多個不同的 JavaScript 包,僅在必要時將各自包發(fā)送到客戶端加派?這就是 Suspense 和 React.lazy 的用武之地。

使用 Suspense 和 React.lazy

React.lazy 的核心功能是傳入一個返回 Promise 的函數(shù)跳芳,以懶加載 React 組件芍锦,Promise 最終會解析為一個組件。大多數(shù)情況下飞盆,我們使用動態(tài)導(dǎo)入語法來懶加載模塊:

const Post = lazy(() => import('./Post.ts'));

結(jié)合 Suspense娄琉,我們可以在導(dǎo)入加載期間向 React 指示渲染一個回退加載狀態(tài):

export default function Wrapper() {
  return (
    <Suspense fallback={<div>加載中...</div>}>
      <Post />
    </Suspense>
  );
}

如果使用了像 React Router 這樣的導(dǎo)航庫次乓,可以通過路由對應(yīng)用進行代碼拆分,在各頁面的 Route 組件中分別懶加載入口點孽水。

當(dāng)然票腰,可以自行實現(xiàn)這種行為——在動態(tài)導(dǎo)入組件時渲染加載狀態(tài)——但使用 Suspense 會更優(yōu)雅。這也引發(fā)了一個問題:能否用 Suspense 簡化所有使用 useEffect 完成的數(shù)據(jù)獲取操作女气?
在 useEffect 中使用 Suspense 進行數(shù)據(jù)獲取

在進一步探討之前杏慰,我們先來簡單了解一下 <Suspense /> 的內(nèi)部機制。主要問題是:父級 <Suspense /> 是如何知道子組件正在加載的炼鞠?據(jù)我所知缘滥,只有兩種方法可以讓子組件改變其父組件的狀態(tài):

  1. 子組件修改父組件使用的狀態(tài)
  2. 子組件拋出一個值,父組件可以捕獲并處理

第二種方式通常表現(xiàn)為一個錯誤邊界(Error Boundary)谒主,這是一個 React 組件朝扼,專門用于捕獲子組件在程序崩潰時拋出的非預(yù)期錯誤。有趣的是霎肯,React 還將這一機制應(yīng)用到了其他場景:Suspense 依賴子組件拋出一個 Promise擎颖。

簡單來說,子組件會在加載時拋出一個掛起狀態(tài)的 Promise观游,并在準(zhǔn)備好渲染時解析肠仪。父組件會捕獲該 Promise,并根據(jù)情況渲染 fallback 屬性或子組件內(nèi)容备典。

可以想象异旧,設(shè)置一個支持 suspense 的數(shù)據(jù)獲取工具是非常復(fù)雜的,使用一個現(xiàn)成的庫會更加省心提佣。

這就是說吮蛹,如果你的庫支持 suspense,你可以將組件包裹在 <Suspense /> 中拌屏,指定一個回退狀態(tài)潮针,并添加一個錯誤邊界以捕獲被拒絕的 Promise,省去手動管理 isLoadingisError 狀態(tài)的麻煩倚喂!

function Post() {
  // 例如每篷,使用 React Query
  const { data } = useQuery({ suspense: true });

  // 可以假設(shè)數(shù)據(jù)已完全獲取,因為在數(shù)據(jù)加載時 React 
  // 會“掛起”組件端圈,而這段代碼不會執(zhí)行焦读!
  return <div>{data}</div>;
}

export default function Wrapper() {
  return (
    <ErrorBoundary fallbackRender={<div>出錯了!</div>}>
      <Suspense fallback={<div>加載中...</div>}>
        <Post />
      </Suspense>
    </ErrorBoundary>
  );
}

對于一些人來說舱权,這看起來更簡潔矗晃;但對另一些人而言,這可能是“毀滅金字塔”(pyramid of doom)的早期跡象宴倍,他們更愿意自己處理加載和錯誤狀態(tài)张症。無論如何仓技,很難說 Suspense 的數(shù)據(jù)獲取開啟了無法手動實現(xiàn)的全新功能——那些功能還會陸續(xù)登場。

服務(wù)端渲染(Server-Side Rendering)

在服務(wù)端渲染應(yīng)用中俗他,Suspense 開啟了一些非常有趣的新功能——但首先脖捻,我們需要簡要了解一下水合(hydration)的基礎(chǔ)。

在請求時兆衅,元框架(如 Next.js)通過運行相關(guān)文件中導(dǎo)出的組件來生成頁面的 HTML地沮,用于首次渲染。這段 HTML 會發(fā)送給用戶涯保,以便在 JavaScript 包加載時看到有意義的內(nèi)容诉濒。當(dāng) JavaScript 到達(dá)時,元框架會在客戶端重新運行組件夕春,確保生成的 DOM 與服務(wù)器上生成的 HTML 相同(若不一致未荒,通常會有警告)。此時及志,除了生成狀態(tài)片排、綁定事件等 JavaScript 功能也已到位。重新在客戶端運行組件的整個過程稱為水合速侈。

相比客戶端渲染率寡,服務(wù)端渲染在首次加載頁面時提供了更好的用戶體驗,因為用戶可以看到服務(wù)器生成的 HTML倚搬,而 JavaScript 包正在加載和執(zhí)行中冶共。不過,僅僅是“看到”——沒有 JavaScript 頁面無法交互每界。問題在于捅僵,整個頁面在可以交互之前都需要被水合完成!這就是 Suspense 的第三個用例:選擇性水合眨层。

通過將組件包裹在 Suspense 中庙楚,React 會將其與頁面的其余部分分開水合。乍看之下趴樱,這似乎沒什么用:如果所有非水合的 HTML 都同時發(fā)送到客戶端馒闷,整個頁面還是會一起被水合。然而叁征,這不完全正確纳账,原因如下:

  1. 如果將多個組件包裹在 Suspense 中,React 可以根據(jù)用戶當(dāng)前的交互航揉,智能決定先水合哪個部分塞祈。也就是說,React 可以優(yōu)先水合頁面的某部分帅涂,從而讓用戶先使用到一個交互控件议薪,同時后臺繼續(xù)水合頁面的其余部分。對處理 JavaScript 較慢的設(shè)備而言媳友,這可以極大地提升用戶體驗斯议。

  2. 在流式傳輸架構(gòu)中,頁面的不同部分可以分開發(fā)送到客戶端醇锚,這意味著某塊 HTML 可以在其他部分還在服務(wù)器渲染時先發(fā)送到客戶端并選擇性地進行水合哼御!

需要注意的是,當(dāng)前的 SSR 框架并不支持選擇性水合——據(jù)我所知焊唬,只有使用 app 目錄的 Next.js 應(yīng)用支持將客戶端組件渲染成服務(wù)器上的 HTML 并進行選擇性水合恋昼。可以查看我關(guān)于 SSR 與服務(wù)器組件的博文了解更多信息赶促!

服務(wù)器組件(Server Components)

服務(wù)器組件簡單來說是指在服務(wù)器上渲染為 HTML 的 React 組件液肌,然后將其發(fā)送到客戶端。聽起來像是服務(wù)端渲染鸥滨,但服務(wù)器組件僅在服務(wù)器端運行嗦哆,永不在客戶端執(zhí)行。它們不能使用事件處理器婿滓、狀態(tài)或 Hooks老速,因此本質(zhì)上是非交互式的。服務(wù)器組件主要用于獲取和渲染靜態(tài)數(shù)據(jù):

export default async function Post() {
  const data = await fetch(...)

  return <div>{data}</div>
}

注意這里的函數(shù)/組件是異步的凸主!可以直接等待數(shù)據(jù)加載完成橘券,然后將內(nèi)容渲染為 HTML 并發(fā)送到客戶端。這個方式簡單優(yōu)雅卿吐,但用戶體驗欠佳——在異步操作完成前旁舰,組件會被阻塞,用戶無法看到任何內(nèi)容但两!這種情況正是加載狀態(tài)的用武之地鬓梅。那么,如何為服務(wù)器組件提供一個加載狀態(tài)谨湘?可以使用 Suspense绽快。

將異步服務(wù)器組件包裹在 <Suspense /> 中,React 會在組件獲取數(shù)據(jù)時渲染并發(fā)送 fallback 到客戶端紧阔。一旦數(shù)據(jù)加載完成坊罢,React 就會發(fā)送組件的渲染內(nèi)容。這種分階段將多塊 HTML 發(fā)送到客戶端的過程稱為流式傳輸(streaming)擅耽。

async function Post() {
  const data = await fetch(...)

  return <div>{data}</div>
}

export default function Wrapper() {
  return (
    <Suspense fallback={<div>加載中...</div>}>
      <Post />
    </Suspense>
  );
}

總結(jié)

以上就是 React Suspense 在三種不同架構(gòu)中的四種用例活孩。至于是否一個 API 能夠適用于如此多樣的場景,留待讀者自行判斷乖仇,但希望本文能幫助您更好地理解這個機制憾儒。感謝閱讀询兴!

資源

  • React 18 的新 Suspense SSR 架構(gòu)
  • 分流工作:使用 Suspense 實現(xiàn)流式服務(wù)端渲染:Shaundai Person
  • React.lazy 的 react.dev 文檔和 MDN 動態(tài)導(dǎo)入文檔
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市起趾,隨后出現(xiàn)的幾起案子诗舰,更是在濱河造成了極大的恐慌,老刑警劉巖训裆,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眶根,死亡現(xiàn)場離奇詭異,居然都是意外死亡边琉,警方通過查閱死者的電腦和手機属百,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來变姨,“玉大人族扰,你說我怎么就攤上這事∏。” “怎么了别伏?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長忧额。 經(jīng)常有香客問我厘肮,道長,這世上最難降的妖魔是什么睦番? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任类茂,我火速辦了婚禮,結(jié)果婚禮上托嚣,老公的妹妹穿的比我還像新娘巩检。我一直安慰自己,他們只是感情好示启,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布兢哭。 她就那樣靜靜地躺著,像睡著了一般夫嗓。 火紅的嫁衣襯著肌膚如雪迟螺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天舍咖,我揣著相機與錄音矩父,去河邊找鬼。 笑死排霉,一個胖子當(dāng)著我的面吹牛窍株,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼球订,長吁一口氣:“原來是場噩夢啊……” “哼后裸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起辙售,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤轻抱,失蹤者是張志新(化名)和其女友劉穎飞涂,沒想到半個月后旦部,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡较店,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年士八,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梁呈。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡婚度,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出官卡,到底是詐尸還是另有隱情蝗茁,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布寻咒,位于F島的核電站哮翘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏毛秘。R本人自食惡果不足惜饭寺,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叫挟。 院中可真熱鬧艰匙,春花似錦、人聲如沸抹恳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奋献。三九已至健霹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秽荞,已是汗流浹背骤公。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扬跋,地道東北人阶捆。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親洒试。 傳聞我的和親對象是個殘疾皇子倍奢,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容