React?SSR架構Stream?Rendering與Suspense?for?Data?Fetching
前言
之前寫過一篇文章介紹了 React 18 SSR 新架構,今天繼續(xù)基于此架構實戰(zhàn)一下。假設我們的業(yè)務背景如下:

我們的頁面分為兩大塊,上面部分是個人介紹,依賴接口 /api/profile (耗時約 3s),下面部分是文章列表,依賴接口 /api/list(耗時約 6s)。其中文章列表的業(yè)務邏輯非常重,代碼體積很大,依賴的接口比較慢。
我們先來看下傳統(tǒng)的 SSR (服務端獲取到所有接口數(shù)據(jù)后調用 renderToString 渲染出內容返回給前端,同時在頁面中插入全局的 INITIAL_STATE 供客戶端注水)和基于 Stream Rendering & Suspense for Data Fetching (以下簡稱 Stream SSR)兩者的效果對比:
| 傳統(tǒng) SSR | Stream SSr |
|---|---|
![]() | ![]() |
首先,我們來對比下從用戶發(fā)起請求到用戶看到內容這個階段。傳統(tǒng) SSR 用戶看到的都是一個空白頁面,一直要等到最耗時的 /api/list 接口返回用戶才能看到內容。而 Stream SSR 有以下幾點的提升:
- 在頁面內容返回前有 loading 的提示
Profile的內容先處理完,先返回,沒有被List阻塞
同樣的,注水過程也是如此。傳統(tǒng) SSR 需要等到 JS 加載完后,統(tǒng)一對整個應用進行注水。而 Stream SSR 則先完成了 Profile 的注水。
那么,要怎么實現(xiàn)這樣的效果呢?接下來讓我們 step by step?;蛘咧苯涌?a rel="external nofollow" target="_blank">代碼。
React SSR Stream Rendering & Suspense for Data Fetching 實踐
Stream Rendering
首先,為了實現(xiàn) Stream Rendering,我們需要使用 renderToPipeableStream,假設我們有如下 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>
則我們可以按如下方式進行返回:
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)
},
})
})
看著有點奇怪是吧,這是因為 renderToPipeableStream 的返回不再是 Node.js 中的 ReadableStream 對象,無法監(jiān)聽 end 事件。所以這里通過一個中間的 Writable 對象來轉接數(shù)據(jù),并監(jiān)測渲染流的結束。
Stream Rendering 的部分搞定了,接下來我們看看 Data Fetching 部分。
Suspense for Data Fetching
在這篇文章曾經(jīng)提到過結合 Suspense 做 Data Fetching,但是之前是自己實現(xiàn)的一個簡單的請求工具,為了更貼近實際,這次使用 react-query。則組件中可以按照如下方式來請求數(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>
)
}
在使用該組件的時候,可以用 Suspense 包裹起來,以便于數(shù)據(jù)返回前用戶可以看到一個 loading 的效果:
const App = () => {
return (
<div>
<Suspense fallback={<p>Loading List...</p>}>
<List />
</Suspense>
...
</div>
)
}
同時為了減少入口文件的體積,我們通過異步的方式來引入 List 這個比較大的組件:
const List = React.lazy(() => import('./List'))
類似的,Profile 組件也可以按照同樣的方式來處理。
這樣,Stream Rendering & Suspense for Data Fetching 基本上算是實現(xiàn)了。不過現(xiàn)在還有個問題,對于每個組件,我們會分別在服務端和客戶端都請求一次接口。正確的做法應該是只在服務端請求一次,然后服務端返回 HTML 的時候把接口數(shù)據(jù)也一并帶上,作為 CSR 的初始數(shù)據(jù)。
React Query 官網(wǎng)中有介紹 SSR 相關的內容,但是跟傳統(tǒng)的 SSR 沒什么區(qū)別,也是要等到數(shù)據(jù)都獲取完后,才開始渲染:
function handleRequest (req, res) {
const queryClient = new QueryClient()
await queryClient.prefetchQuery('key', fn)
const dehydratedState = dehydrate(queryClient) // 得到一個接口請求的全局狀態(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()
}
這樣的做法有幾個缺點:
- 整個應用的渲染都被阻塞了,原本可以更早返回的
Profile也被推遲了 - 必須要知道當前頁面渲染所需要調用的所有接口,當頁面很復雜且由多人維護時這個代碼就很不好維護了
下面我們來解決這些問題,最終的方案我稱之為“全局狀態(tài)動態(tài)更新”方案。
全局狀態(tài)動態(tài)更新
從上面的代碼可以知道,通過 dehydrate(queryClient) 可以得到一個全局的對象用來描述當前請求得到的數(shù)據(jù),那我們是不是可以在組件里面每次有數(shù)據(jù)獲取到時就來更新一下這個對象呢?就像這樣:
const query = useQuery(['data'], getList)
const ee = useContext(EventEmitterContext)
if (ee && query.data) {
ee.emit('updateState')
}
然后我們在處理請求的回調函數(shù)中監(jiān)聽這個事件,更新全局狀態(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
)};`
})
這樣我們就做到了仍然流式的返回內容給用戶,并在這個過程中不停的更新全局數(shù)據(jù),最后返回給客戶端,而且也不需要了解這個頁面渲染所需要的所有接口,即保證了用戶體驗,又沒有丟失代碼的可維護性。
React 團隊一直在提高用戶體驗、代碼可維護性和性能這些方面進行不停的探索,感覺不久的將來前端的開發(fā)方式又要被他們改變了,看來前端還沒有死,還能繼續(xù)折騰。
以上就是React SSR架構Stream Rendering與Suspense for Data Fetching的詳細內容,更多關于React SSR架構的資料請關注腳本之家其它相關文章!
相關文章
react-draggable實現(xiàn)拖拽功能實例詳解
這篇文章主要給大家介紹了關于react-draggable實現(xiàn)拖拽功能的相關資料,React-Draggable一個使元素可拖動的簡單組件,文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下2023-08-08
React使用高德地圖的實現(xiàn)示例(react-amap)
這篇文章主要介紹了React使用高德地圖的實現(xiàn)示例(react-amap),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04
React組件如何優(yōu)雅地處理異步數(shù)據(jù)詳解
這篇文章主要為大家介紹了React組件如何優(yōu)雅地處理異步數(shù)據(jù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
React Hook父組件如何獲取子組件的數(shù)據(jù)/函數(shù)
這篇文章主要介紹了React Hook父組件如何獲取子組件的數(shù)據(jù)/函數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09



