React?SSR架構(gòu)Stream?Rendering與Suspense?for?Data?Fetching
前言
之前寫過(guò)一篇文章介紹了 React 18 SSR 新架構(gòu),今天繼續(xù)基于此架構(gòu)實(shí)戰(zhàn)一下。假設(shè)我們的業(yè)務(wù)背景如下:
我們的頁(yè)面分為兩大塊,上面部分是個(gè)人介紹,依賴接口 /api/profile
(耗時(shí)約 3s),下面部分是文章列表,依賴接口 /api/list
(耗時(shí)約 6s)。其中文章列表的業(yè)務(wù)邏輯非常重,代碼體積很大,依賴的接口比較慢。
我們先來(lái)看下傳統(tǒng)的 SSR (服務(wù)端獲取到所有接口數(shù)據(jù)后調(diào)用 renderToString
渲染出內(nèi)容返回給前端,同時(shí)在頁(yè)面中插入全局的 INITIAL_STATE 供客戶端注水)和基于 Stream Rendering & Suspense for Data Fetching (以下簡(jiǎn)稱 Stream SSR)兩者的效果對(duì)比:
傳統(tǒng) SSR | Stream SSr |
---|---|
![]() | ![]() |
首先,我們來(lái)對(duì)比下從用戶發(fā)起請(qǐng)求到用戶看到內(nèi)容這個(gè)階段。傳統(tǒng) SSR 用戶看到的都是一個(gè)空白頁(yè)面,一直要等到最耗時(shí)的 /api/list
接口返回用戶才能看到內(nèi)容。而 Stream SSR 有以下幾點(diǎn)的提升:
- 在頁(yè)面內(nèi)容返回前有 loading 的提示
Profile
的內(nèi)容先處理完,先返回,沒(méi)有被List
阻塞
同樣的,注水過(guò)程也是如此。傳統(tǒng) SSR 需要等到 JS 加載完后,統(tǒng)一對(duì)整個(gè)應(yīng)用進(jìn)行注水。而 Stream SSR 則先完成了 Profile
的注水。
那么,要怎么實(shí)現(xiàn)這樣的效果呢?接下來(lái)讓我們 step by step。或者直接看代碼。
React SSR Stream Rendering & Suspense for Data Fetching 實(shí)踐
Stream Rendering
首先,為了實(shí)現(xiàn) Stream Rendering,我們需要使用 renderToPipeableStream
,假設(shè)我們有如下 HTML 模板:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>SSR + MicroFrontend</title> </head> <body> <div id="app1"><!-- app1 --></div> <script crossorigin src="http://localhost:8080/dist/client.js"></script> </body> </html>
則我們可以按如下方式進(jìn)行返回:
app.get('/', async (req, res) => { const [heal, tail] = html.split('<!-- app1 -->') const stream = new Writable({ write(chunk, _encoding, cb) { res.write(chunk, cb) }, final() { res.end(tail) }, }) const {pipe} = renderToPipeableStream(<App />, { onShellReady() { res.statusCode = 200 res.write(head) pipe(stream) }, }) })
看著有點(diǎn)奇怪是吧,這是因?yàn)?renderToPipeableStream
的返回不再是 Node.js 中的 ReadableStream
對(duì)象,無(wú)法監(jiān)聽(tīng) end
事件。所以這里通過(guò)一個(gè)中間的 Writable
對(duì)象來(lái)轉(zhuǎn)接數(shù)據(jù),并監(jiān)測(cè)渲染流的結(jié)束。
Stream Rendering 的部分搞定了,接下來(lái)我們看看 Data Fetching 部分。
Suspense for Data Fetching
在這篇文章曾經(jīng)提到過(guò)結(jié)合 Suspense
做 Data Fetching,但是之前是自己實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的請(qǐng)求工具,為了更貼近實(shí)際,這次使用 react-query
。則組件中可以按照如下方式來(lái)請(qǐng)求數(shù)據(jù):
async function getList() { const rsp = await fetch('http://localhost:9000/api/list') const data = await rsp.json() return data } const List = () => { const query = useQuery(['list'], getList) return ( <ul> {query.data.map((item) => ( <li key={item.name}>{item.name}</li> ))} </ul> ) }
在使用該組件的時(shí)候,可以用 Suspense
包裹起來(lái),以便于數(shù)據(jù)返回前用戶可以看到一個(gè) loading 的效果:
const App = () => { return ( <div> <Suspense fallback={<p>Loading List...</p>}> <List /> </Suspense> ... </div> ) }
同時(shí)為了減少入口文件的體積,我們通過(guò)異步的方式來(lái)引入 List
這個(gè)比較大的組件:
const List = React.lazy(() => import('./List'))
類似的,Profile
組件也可以按照同樣的方式來(lái)處理。
這樣,Stream Rendering & Suspense for Data Fetching 基本上算是實(shí)現(xiàn)了。不過(guò)現(xiàn)在還有個(gè)問(wèn)題,對(duì)于每個(gè)組件,我們會(huì)分別在服務(wù)端和客戶端都請(qǐng)求一次接口。正確的做法應(yīng)該是只在服務(wù)端請(qǐng)求一次,然后服務(wù)端返回 HTML 的時(shí)候把接口數(shù)據(jù)也一并帶上,作為 CSR 的初始數(shù)據(jù)。
React Query 官網(wǎng)中有介紹 SSR 相關(guān)的內(nèi)容,但是跟傳統(tǒng)的 SSR 沒(méi)什么區(qū)別,也是要等到數(shù)據(jù)都獲取完后,才開(kāi)始渲染:
function handleRequest (req, res) { const queryClient = new QueryClient() await queryClient.prefetchQuery('key', fn) const dehydratedState = dehydrate(queryClient) // 得到一個(gè)接口請(qǐng)求的全局狀態(tài) const html = ReactDOM.renderToString( <QueryClientProvider client={queryClient}> <Hydrate state={dehydratedState}> <App /> </Hydrate> </QueryClientProvider> ) res.send(` <html> <body> <div id="root">${html}</div> <script> window.__REACT_QUERY_STATE__ = ${JSON.stringify(dehydratedState)}; </script> </body> </html> `) queryClient.clear() }
這樣的做法有幾個(gè)缺點(diǎn):
- 整個(gè)應(yīng)用的渲染都被阻塞了,原本可以更早返回的
Profile
也被推遲了 - 必須要知道當(dāng)前頁(yè)面渲染所需要調(diào)用的所有接口,當(dāng)頁(yè)面很復(fù)雜且由多人維護(hù)時(shí)這個(gè)代碼就很不好維護(hù)了
下面我們來(lái)解決這些問(wèn)題,最終的方案我稱之為“全局狀態(tài)動(dòng)態(tài)更新”方案。
全局狀態(tài)動(dòng)態(tài)更新
從上面的代碼可以知道,通過(guò) dehydrate(queryClient)
可以得到一個(gè)全局的對(duì)象用來(lái)描述當(dāng)前請(qǐng)求得到的數(shù)據(jù),那我們是不是可以在組件里面每次有數(shù)據(jù)獲取到時(shí)就來(lái)更新一下這個(gè)對(duì)象呢?就像這樣:
const query = useQuery(['data'], getList) const ee = useContext(EventEmitterContext) if (ee && query.data) { ee.emit('updateState') }
然后我們?cè)谔幚碚?qǐng)求的回調(diào)函數(shù)中監(jiān)聽(tīng)這個(gè)事件,更新全局狀態(tài):
const templateDOM = new JSDOM(` <!DOCTYPE html> <html lang="en"> <head> ... </head> <body> <div id="app1"><!-- app1 --></div> <script id="reactQueryState">window.__REACT_QUERY_STATE__ = ${JSON.stringify( dehydratedState )};</script> ... </body> </html> `) ... ee.on('updateState', () => { const dehydratedState = dehydrate(queryClient) templateDoc.querySelector( '#reactQueryState' ).innerHTML = `window.__REACT_QUERY_STATE__ = ${JSON.stringify( dehydratedState )};` })
這樣我們就做到了仍然流式的返回內(nèi)容給用戶,并在這個(gè)過(guò)程中不停的更新全局?jǐn)?shù)據(jù),最后返回給客戶端,而且也不需要了解這個(gè)頁(yè)面渲染所需要的所有接口,即保證了用戶體驗(yàn),又沒(méi)有丟失代碼的可維護(hù)性。
React 團(tuán)隊(duì)一直在提高用戶體驗(yàn)、代碼可維護(hù)性和性能這些方面進(jìn)行不停的探索,感覺(jué)不久的將來(lái)前端的開(kāi)發(fā)方式又要被他們改變了,看來(lái)前端還沒(méi)有死,還能繼續(xù)折騰。
以上就是React SSR架構(gòu)Stream Rendering與Suspense for Data Fetching的詳細(xì)內(nèi)容,更多關(guān)于React SSR架構(gòu)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React?Suspense解決競(jìng)態(tài)條件詳解
這篇文章主要為大家介紹了React?Suspense解決競(jìng)態(tài)條件詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11React組件對(duì)子組件children進(jìn)行加強(qiáng)的方法
這篇文章主要給大家介紹了關(guān)于React組件中對(duì)子組件children進(jìn)行加強(qiáng)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用React具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06react-draggable實(shí)現(xiàn)拖拽功能實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于react-draggable實(shí)現(xiàn)拖拽功能的相關(guān)資料,React-Draggable一個(gè)使元素可拖動(dòng)的簡(jiǎn)單組件,文中通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08React使用高德地圖的實(shí)現(xiàn)示例(react-amap)
這篇文章主要介紹了React使用高德地圖的實(shí)現(xiàn)示例(react-amap),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04探討JWT身份校驗(yàn)與React-router無(wú)縫集成
這篇文章主要為大家介紹了JWT身份校驗(yàn)與React-router無(wú)縫集成的探討解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06React組件如何優(yōu)雅地處理異步數(shù)據(jù)詳解
這篇文章主要為大家介紹了React組件如何優(yōu)雅地處理異步數(shù)據(jù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10React Hook父組件如何獲取子組件的數(shù)據(jù)/函數(shù)
這篇文章主要介紹了React Hook父組件如何獲取子組件的數(shù)據(jù)/函數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09