前言
2020 年底,React 公布了一個(gè)全新的特性:Server Components,當(dāng)時(shí)它還處于調(diào)研和試驗(yàn)階段,并沒(méi)有正式發(fā)布,隨著 React 18.0 版本的正式發(fā)布,Server Component 的腳步聲也越來(lái)越近了,不出意外的話,應(yīng)該會(huì)在今年的某個(gè) React 18 的 minor 版本中正式發(fā)布。
Server Components 聽(tīng)起來(lái)好像并不那么激動(dòng)人心,React 18 所發(fā)布的各種特性也似乎平平無(wú)奇,自從 Hooks 面世已經(jīng)三年多過(guò)去了,React 似乎停滯了前進(jìn)的腳步,只是在現(xiàn)有的基礎(chǔ)上做些小修小補(bǔ)?
No。
Concurrent rendering(React 18 新帶來(lái)的特性)是一種本質(zhì)上的改變,它本身不像 Hooks 那樣對(duì)開(kāi)發(fā)體驗(yàn)有著近乎翻天覆地的變革,但是這種底層渲染能力/機(jī)制的調(diào)整,會(huì)帶來(lái)非常非常多的可能性,例如:
Suspense、OffScreen、Server Components
這三種特性,目前都沒(méi)有生產(chǎn)可用,但是等到未來(lái)他們正式發(fā)布并漸漸被大面積使用時(shí),每一項(xiàng)特性都會(huì)帶來(lái)非常顯著的開(kāi)發(fā)體驗(yàn)的提升。而如果讓我從這些未來(lái)會(huì)出現(xiàn)的新特性中選一個(gè)最期待的,那毫不疑問(wèn)會(huì)是 Server Component。
所以,Server Components 到底是什么?他會(huì)像當(dāng)年的 Hooks 一樣對(duì)整個(gè) React 生態(tài)帶來(lái)巨大的影響么?在我們回答這些問(wèn)題之前,很有必要先解釋一下 Server Components 是什么,又解決了什么問(wèn)題。
注:下文中的很多內(nèi)容受 Dan 和 Lauren 的這份 演講視頻 [1] 所啟發(fā),如果你想更深入的了解即將到來(lái)的 React Server Component,那么非常推薦這段視頻 事實(shí)上,這篇文章并不是一份對(duì) Server Components 的用法教學(xué),也不會(huì)涵蓋 Server Components 的每一處細(xì)節(jié)(甚至為了方便表述會(huì)有意地略過(guò)一些細(xì)節(jié)),因此, 在讀下文之前,最好是對(duì) Server Components 已經(jīng)有所了解
背景:前后端分離
“前后端分離”是當(dāng)下主流的 web 研發(fā)模式,后端存儲(chǔ)數(shù)據(jù),并把對(duì)數(shù)據(jù)的操作(增刪改查)封裝成接口,通過(guò)后端服務(wù)提供給前端,前端應(yīng)用發(fā)送請(qǐng)求(例如 http 請(qǐng)求或者 rpc 請(qǐng)求)去調(diào)用后端提供的接口,從而獲取到數(shù)據(jù)或者是對(duì)數(shù)據(jù)進(jìn)行修改。
這可能是十幾年以來(lái)非常普遍的研發(fā)模式了,也因此,我們被區(qū)分成前端開(kāi)發(fā)和后端開(kāi)發(fā),各自負(fù)責(zé)著“楚河漢界”的一側(cè)。我們?cè)诟髯阅且粋?cè)都做了非常多的優(yōu)化、創(chuàng)新、突破,在后端,我們有容器化、微服務(wù)、SSR,在前端,我們有 code spliting、前端路由、React Hooks。
但是對(duì)于 API 層,我們似乎這么多年以來(lái)都未曾有過(guò)關(guān)注,即便是有,也僅僅是停留于 API 傳輸性能(例如 grpc)、API 的存在形式(例如 Restful 和 GraphQL)、API 的工程化管理(例如 Postman)。
并非是想說(shuō) API 一個(gè)邪惡而糟糕的設(shè)計(jì),但是自從 Restful 的概念被提出以來(lái),已經(jīng) 22 年過(guò)去了,我們是不是應(yīng)該在現(xiàn)在重新思考一下:
- 以網(wǎng)絡(luò)請(qǐng)求作為前后端的分界是最優(yōu)解嗎?
- 如果沒(méi)有 API,我們?cè)撊绾渭軜?gòu)和開(kāi)發(fā) Web 應(yīng)用?
癥結(jié)所在
讓我們?cè)倩氐絼倓偟哪菑垐D,考慮一下 API 在帶來(lái)職責(zé)分工明晰之外,同時(shí)也帶來(lái)了哪些問(wèn)題。
請(qǐng)求瀑布流(Waterfall)
就像 Remix [2] 首頁(yè)上所展示的,基于 API 和嵌套路由的前端站點(diǎn),在請(qǐng)求時(shí)會(huì)出現(xiàn)瀑布流的現(xiàn)象:
數(shù)據(jù)的之間可能是有前后的依賴關(guān)系,抑或是和組件強(qiáng)耦合在一起,需要等待組件的 bundle 加載完成之后才能發(fā)出請(qǐng)求,這些都導(dǎo)致了請(qǐng)求瀑布流現(xiàn)象的出現(xiàn)。
并發(fā)請(qǐng)求
后端希望實(shí)現(xiàn)小而美的接口,每個(gè)接口有獨(dú)立的職責(zé),例如:
- getUser 獲取用戶信息
- getSongs?page=12 獲取歌曲列表
- getNotifactions 獲取通知列表
- getFavoirateSongs 獲取收藏的歌曲
- getNewSongs 獲取新發(fā)布的歌曲
- getRecommendSong 獲取今日推薦的歌曲及對(duì)應(yīng)的文案
- getSearchBarHotKeywords 獲取熱門的搜索詞
- getAdBanner 獲取廣告 banner 內(nèi)容
- getRecentSongs 獲取最近聽(tīng)歌記錄
- getRecommendedPlayList 獲取推薦的歌單列表
- ……(實(shí)在太多了)
每一個(gè)接口,單獨(dú)拿出來(lái)看都是合理的,但是放在一起,就會(huì)發(fā)現(xiàn)用戶每次打開(kāi)這樣一個(gè)音樂(lè) web App,都要發(fā)送至少十幾個(gè)接口,對(duì)于一些稍微復(fù)雜一點(diǎn)的網(wǎng)頁(yè),首次加載就需要請(qǐng)求幾十個(gè)接口也絲毫不奇怪。
每一個(gè)接口的請(qǐng)求,都會(huì)帶來(lái)網(wǎng)絡(luò)開(kāi)銷,甚至在有些環(huán)境下會(huì)有最大并發(fā)請(qǐng)求數(shù)量的限制(例如在支付寶客戶端那的 rpc 請(qǐng)求),或許網(wǎng)絡(luò)層的 automatic batching 可以解決這個(gè)問(wèn)題,但是遺憾的是,在目前的技術(shù)體系內(nèi),這個(gè)問(wèn)題并不好解決(這里沒(méi)有寫不能解決,是因?yàn)榈拇_有一些可行的方案,例如 BFF、依賴網(wǎng)關(guān)來(lái)做接口聚合,但它們都引入的新的問(wèn)題)。
前端包體積(Bundle size)
包體積已經(jīng)是“現(xiàn)代”前端開(kāi)發(fā)領(lǐng)域飽受詬病的一點(diǎn)了,動(dòng)輒幾百 k 的 js 文件,似乎已經(jīng)背離了瀏覽器是用來(lái)“瀏覽”網(wǎng)頁(yè)的初衷了。并不是說(shuō)我們都要做一個(gè)瀏覽器原教旨主義者,但是如果網(wǎng)頁(yè)能夠在不損失用戶體驗(yàn)和開(kāi)發(fā)體驗(yàn)的前提下,恢復(fù)到非常輕量和快速的狀態(tài),難道不是一件好事么?
協(xié)作成本(溝通、邏輯感知和封閉)
在我個(gè)人看來(lái),這是大型項(xiàng)目或需要長(zhǎng)期維護(hù)的應(yīng)用中最令人頭疼的問(wèn)題了。
假設(shè)我們現(xiàn)在有一個(gè)非常巨大的應(yīng)用,需要有十幾位開(kāi)發(fā)者共同編寫和維護(hù),那如何分工?答案必然是先做模塊化,我們把整個(gè)應(yīng)用拆分成幾個(gè)彼此盡量獨(dú)立的模塊,再由每個(gè)人或每幾個(gè)人負(fù)責(zé)其中的一個(gè)模塊。
模塊化帶來(lái)的好處是邊界清晰(看到一個(gè)需求就能判斷出來(lái)涉及到哪個(gè)或哪些模塊做哪些改動(dòng))、職責(zé)明確(每個(gè)人都有自己確定的職責(zé))、減少溝通成本(由于模塊內(nèi)部的邏輯是封閉的,不需要外部感知,所以可以降低溝通成本)。
對(duì)于前兩點(diǎn),目前的前后端分離架構(gòu)都還是及格的,但對(duì)于第三點(diǎn),我覺(jué)得基于網(wǎng)絡(luò)請(qǐng)求接口的協(xié)作模式,在很多情況下并沒(méi)有有效地做到邏輯內(nèi)部封閉、減少需要前后端之間來(lái)回溝通的信息量。
舉個(gè)例子,對(duì)于這樣的一個(gè)頁(yè)面:
看起來(lái)非常簡(jiǎn)單,一些信息的展示,加上一個(gè)充值按鈕,這就是我最開(kāi)始所設(shè)想的。
然而,隨著這個(gè)項(xiàng)目不斷的推進(jìn),我發(fā)現(xiàn),原本以為是純靜態(tài)的標(biāo)題文案,實(shí)際上是需要后端控制的,根據(jù)當(dāng)前用戶的所屬人群來(lái)動(dòng)態(tài)判斷文案內(nèi)容;我發(fā)現(xiàn),由于前端金額計(jì)算的可靠性問(wèn)題,折扣和實(shí)際支付相關(guān)的內(nèi)容都是需要在后端預(yù)處理之后展示在前端的;
我發(fā)現(xiàn),倒計(jì)時(shí)的參考時(shí)間是需要依靠后端返回的;我發(fā)現(xiàn),按鈕的文案、點(diǎn)擊行為,是需要后端控制的,特別是按鈕的點(diǎn)擊行為,最終方案是后端返回一個(gè)枚舉,前端根據(jù)這個(gè)值來(lái) switch case 一下走不同的邏輯(例如下單、引導(dǎo)先進(jìn)行注冊(cè)和綁卡)……
為了閱讀體驗(yàn),我只是列舉了其中隨手想到的一小部分,如果總結(jié)一下,那就是,后端和前端并沒(méi)有因?yàn)?ldquo;前后端分離”而做到解藕,反倒是藕斷絲連,剪不斷理還亂。后端感知了過(guò)多的前端視圖層邏輯,就像是發(fā)明了一套 DSL(Domain Specific Language),而前端則是要寫一個(gè)針對(duì)這套 DSL 的解析器和渲染器。
回到我們剛剛提到的,模塊化帶來(lái)的好處。模塊化能夠降低溝通成本,有一個(gè)不可忽略前提,就是架構(gòu)的合理性。模塊化并非是降低溝通成本的本質(zhì)原因,也并非所有的模塊化實(shí)踐都能帶來(lái)溝通成本的降低。當(dāng)前后端分離的實(shí)踐成為一個(gè)僵硬的、死板的“規(guī)范”,那它還能真正起到多少降低溝通成本的作用?一個(gè)大大的問(wèn)號(hào)。
Server Components
再次申明一下,下文是假設(shè)讀者朋友已經(jīng)對(duì) Server Components [3] 有所了解
基于網(wǎng)絡(luò)請(qǐng)求的 API 模型,有一個(gè)大大的前提假設(shè),就是前端應(yīng)用和后端應(yīng)用是兩個(gè)獨(dú)立的應(yīng)用,但是為什么一定要是這樣?
或許我們可以讓后端應(yīng)用直接渲染 html,用戶操作時(shí),重新渲染一遍頁(yè)面?這其實(shí)就是在 Restful 時(shí)代之前的架構(gòu),有很多弊端,特別是可交互性差,不然也就不會(huì)出現(xiàn)后來(lái) Restful 的盛行了。
那再或許,我們可以讓前端的 React 組件,運(yùn)行在后端?
這就是 React Server Components。
一圖勝千言,在現(xiàn)在的前后端分離模式下,后端提供接口,前端的 React 組件調(diào)用接口。
而如果后端可以運(yùn)行 React 組件,直接渲染 React 節(jié)點(diǎn)樹(shù)到前端,就不需要所謂的 API 的概念了。
后端運(yùn)行 React 組件并不是什么新鮮事,我們?cè)?SSR(Server Side Rending)早就習(xí)以為常了,但是需要特別注明的一點(diǎn)是,在 SSR 中,后端是運(yùn)行了 React 組件,生成了一份初始狀態(tài)的 html,但這份 html 是沒(méi)有可交互性的,它只是為了讓用戶能盡早看到頁(yè)面而做的一種改良式的、修修補(bǔ)補(bǔ)一樣的優(yōu)化。
而 Server Components 所帶來(lái)的,是我們可以把同一個(gè)項(xiàng)目中,一部分的組件作為 Server Components,另一部分組件,作為 Client Components,因此我們可以既享受到后端內(nèi)部調(diào)用帶來(lái)的便捷、可維護(hù)性,又能保證頁(yè)面的可交互性幾乎沒(méi)有任何妥協(xié)。
如果你用過(guò) php 或 Django,那你肯定非常熟悉這種模式:后端直接渲染 html 內(nèi)容,瀏覽器只負(fù)責(zé)顯示,用戶點(diǎn)擊按鈕,那就重新請(qǐng)求、重新渲染頁(yè)面,如果頁(yè)面上需要一些復(fù)雜的動(dòng)態(tài)交互,比如讓用戶可以把一個(gè)列表展開(kāi)/收起,或者是點(diǎn)擊某個(gè)按鈕之后展示一個(gè)模態(tài)框,那可以借助于 jQuery 來(lái)實(shí)現(xiàn)。
PHP + bootstrap + jQuery,現(xiàn)在,Server Components 就像是這套范式的升級(jí)版,可以被稱為一種全新的“全棧”開(kāi)發(fā)模式。
因?yàn)槭窃诤蠖谁h(huán)境下,這些 Server Components 可以使用全部的后端能力,不管是中間件,還是其他后端微服務(wù)的調(diào)用,甚至是 db 的訪問(wèn)(當(dāng)然可以直接跑 SQL,但是更好的實(shí)踐是通過(guò)一個(gè)數(shù)據(jù)中間層),都可以實(shí)現(xiàn)。這樣一來(lái),我們就可以直接把數(shù)據(jù)從源頭獲取,放到 React 組件的上下文中,那自然就不需要傳統(tǒng)意義上的 API 了。
更準(zhǔn)確的說(shuō),API 并未消失,我們其實(shí)也不會(huì)和 API 就此說(shuō)再見(jiàn),而是讓它換了一種形式。有模塊化的地方,就會(huì)有 API,Restful 的 http 網(wǎng)絡(luò)請(qǐng)求固然是 API,但中間件暴露出來(lái)的方法,瀏覽器提供的 Date 對(duì)象,node 提供的文件讀取函數(shù),db 提供的 SQL,這些全都是 API。
在這種新架構(gòu)下,API 變成了后端里業(yè)務(wù)應(yīng)用和上游服務(wù)之間的調(diào)用,變成了 Server Components 和 Client Components 之間的 props 傳遞,前者讓 API 變得更加干凈、更符合單一職責(zé)的原則,而后者讓 API 變得自然到你幾乎感知不到。
所以:
- Server Components 允許我們不再按照 前端 - 后端 進(jìn)行模塊的拆分,而是依照 業(yè)務(wù)應(yīng)用 - 底層服務(wù) 來(lái)進(jìn)行更合理的模塊拆分。從而可以理論上降低模塊之間的溝通成本(因?yàn)槟壳斑€沒(méi)有辦法實(shí)踐證明)。
- 由于 Server Components 是在后端運(yùn)行組件,直接通過(guò)網(wǎng)絡(luò)傳輸給前端進(jìn)行渲染,因此很多大體積的包(例如 markdown 渲染、html sanitize)都不需要在前端下載和運(yùn)行,從而很大程度上降低包體積。
- 由于底層 db 或上游服務(wù)的調(diào)用都是發(fā)生在后端內(nèi)部的,因此即便出現(xiàn)并發(fā)請(qǐng)求,所帶來(lái)開(kāi)銷也遠(yuǎn)遠(yuǎn)小于前端并發(fā)調(diào)用后端的 Restful API。
- 同理,請(qǐng)求瀑布流的問(wèn)題也會(huì)因?yàn)檎{(diào)用開(kāi)銷降低而消失或減輕。
想象
如果大膽想象一下的話,未來(lái)的研發(fā)模式可能這樣的:
開(kāi)發(fā)者將不會(huì)再區(qū)分前端和后端,而是區(qū)分為業(yè)務(wù)應(yīng)用開(kāi)發(fā)和上游服務(wù)開(kāi)發(fā)?,F(xiàn)在的后端開(kāi)發(fā)將(真正地)不再需要關(guān)注視圖邏輯,只聚焦于底層業(yè)務(wù)邏輯,為前端提供清晰好用、原子化的服務(wù)/接口;而現(xiàn)在的前端開(kāi)發(fā)將會(huì)拓展到橫跨前端和后端(代碼運(yùn)行環(huán)境上),負(fù)責(zé)的是在后端封裝好的一個(gè)個(gè)原子化的底層能力上,構(gòu)建視圖層,而我們也需要一套全新的框架和基礎(chǔ)設(shè)施,來(lái)適配 Server Components。
目前,Server Components 還沒(méi)有正式發(fā)布,而即便正式發(fā)布之后,也還有長(zhǎng)長(zhǎng)的工程化落地的路要走,Server Components 增加了很多額外的限制,server、client、shared 的區(qū)分也可能會(huì)帶來(lái)一些理解成本。緩存、性能、server 重新渲染時(shí)的增量更新策略、發(fā)布時(shí)的可灰度性和可回滾性、業(yè)務(wù)中邊界情況的處理,還有很多的問(wèn)題需要去解決,還有很多的未知尚未被驗(yàn)證。
參考資料
[1] 演講視頻:
https://www.YouTube.com/watch?v=TQQPAU21ZUw
[2] Remix: https://remix.run
[3] Server Components: https://reactjs.org/blog/2020/12/21/data-fetching-with-react-server-components.html