具有串流媒體和動態資料的高級 React SSR 技術
隨著應用程式的成長,挑戰也會隨之增加。為了保持領先地位,掌握先進的 SSR 技術對於提供無縫且高效能的使用者體驗至關重要。
在上一篇文章中為React 專案中的伺服器端渲染奠定了基礎,我很高興與您分享可以幫助您保持專案可擴展性、有效地將資料從伺服器載入到客戶端以及解決水合問題的功能。
目錄
- SSR 中的串流
- 延遲載入和 SSR
-
使用延遲載入實現串流傳輸
- 更新 React 元件
- 更新串流伺服器
-
伺服器到客戶端數據
- 在伺服器上傳遞資料
- 處理客戶端上的環境變數
-
水分問題
- 範例場景
- 解決水分問題
- 結論
SSR 中的串流是什麼
伺服器端渲染(SSR) 中的串流 是一種技術,伺服器在產生HTML 頁面的各個部分時將其以區塊的形式傳送到瀏覽器,而不是等待整個頁面準備好在交付之前。這允許瀏覽器立即開始渲染內容,從而縮短載入時間並提高使用者的效能。
串流媒體對於以下方面特別有效:
- 大頁:產生整個 HTML 可能需要花費大量時間。
- 動態內容:當頁面的某些部分依賴外部 API 呼叫或動態產生的區塊。
- 高流量應用程式:減少高峰使用期間的伺服器負載和延遲。
串流媒體彌合了傳統 SSR 和現代客戶端互動性之間的差距,確保使用者在不影響效能的情況下更快地看到有意義的內容。
延遲載入和 SSR
延遲載入是一種將元件或模組的載入延遲到實際需要時才載入的技術,從而減少初始載入時間並提高效能。與 SSR 結合使用時,延遲載入可以顯著優化伺服器和用戶端工作負載。
延遲載入依賴 React.lazy,它動態地將元件匯入為 Promises。在傳統的 SSR 中,渲染是同步的,這意味著伺服器必須解析所有 Promise,然後才能產生完整的 HTML 並將其發送到瀏覽器。
串流處理允許伺服器在渲染元件時以區塊的形式發送 HTML,從而解決了這些挑戰。這種方法使 Suspense 回退能夠立即發送到瀏覽器,確保用戶儘早看到有意義的內容。當延遲載入的元件被解析時,它們渲染的 HTML 會逐漸傳輸到瀏覽器,無縫地取代後備內容。這可以避免阻塞渲染過程,減少延遲並縮短感知載入時間。
使用延遲載入實現串流傳輸
本指南是基於上一篇文章中介紹的概念,建立生產就緒的 SSR React 應用程式,您可以在底部找到連結。為了透過 React 啟用 SSR 並支援延遲載入元件,我們將對 React 元件和伺服器進行多項更新。
更新 React 元件
伺服器入口點
React 的 renderToString 方法通常用於 SSR,但它會等到整個 HTML 內容準備好後才將其傳送到瀏覽器。透過切換到 renderToPipeableStream,我們可以啟用串流傳輸,它會在產生 HTML 部分時發送它們。
// ./src/entry-server.tsx import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server' import App from './App' export function render(options?: RenderToPipeableStreamOptions) { return renderToPipeableStream(<App />, options) }
建立延遲載入元件
在此範例中,我們將建立一個簡單的 Card 元件來示範該概念。在生產應用程式中,此技術通常與較大的模組或整個頁面一起使用以優化效能。
// ./src/Card.tsx import { useState } from 'react' function Card() { const [count, setCount] = useState(0) return ( <div className="card"> <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> ) } export default Card
在應用程式中使用延遲載入元件
要使用延遲載入元件,請使用 React.lazy 動態匯入它,並用 Suspense 包裝它,以在載入期間提供後備 UI
// ./src/App.tsx import { lazy, Suspense } from 'react' import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import './App.css' const Card = lazy(() => import('./Card')) function App() { return ( <> <div> <a href="https://vite.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <Suspense fallback='Loading...'> <Card /> </Suspense> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> ) } export default App
更新串流媒體伺服器
為了啟用串流,開發和生產設定都需要支援一致的 HTML 渲染過程。由於兩個環境的過程相同,因此您可以建立一個可重複使用函數來有效處理流程內容。
建立流內容函數
// ./server/constants.ts export const ABORT_DELAY = 5000
streamContent 函數啟動渲染過程,將增量 HTML 區塊寫入回應,並確保正確的錯誤處理。
// ./server/streamContent.ts import { Transform } from 'node:stream' import { Request, Response, NextFunction } from 'express' import { ABORT_DELAY, HTML_KEY } from './constants' import type { render } from '../src/entry-server' export type StreamContentArgs = { render: typeof render html: string req: Request res: Response next: NextFunction } export function streamContent({ render, html, res }: StreamContentArgs) { let renderFailed = false // Initiates the streaming process by calling the render function const { pipe, abort } = render({ // Handles errors that occur before the shell is ready onShellError() { res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }
更新開發配置
// ./server/dev.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import { StreamContentArgs } from './streamContent' const HTML_PATH = path.resolve(process.cwd(), 'index.html') const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx') // Add to args the streamContent callback export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) { const vite = await ( await import('vite') ).createServer({ root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', }) app.use(vite.middlewares) app.get('*', async (req, res, next) => { try { let html = fs.readFileSync(HTML_PATH, 'utf-8') html = await vite.transformIndexHtml(req.originalUrl, html) const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH) // Use the same callback for production and development process streamContent({ render, html, req, res, next }) } catch (e) { vite.ssrFixStacktrace(e as Error) console.error((e as Error).stack) next(e) } }) }
更新生產配置
// ./server/prod.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import compression from 'compression' import sirv from 'sirv' import { StreamContentArgs } from './streamContent' const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client') const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html') const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js') // Add to Args the streamContent callback export async function setupProd(app: Application, streamContent: (args: StreamContentArgs) => void) { app.use(compression()) app.use(sirv(CLIENT_PATH, { extensions: [] })) app.get('*', async (req, res, next) => { try { const html = fs.readFileSync(HTML_PATH, 'utf-8') const { render } = await import(ENTRY_SERVER_PATH) // Use the same callback for production and development process streamContent({ render, html, req, res, next }) } catch (e) { console.error((e as Error).stack) next(e) } }) }
更新 Express 伺服器
將streamContent函數傳遞給每個配置:
// ./server/app.ts import express from 'express' import { PROD, APP_PORT } from './constants' import { setupProd } from './prod' import { setupDev } from './dev' import { streamContent } from './streamContent' export async function createServer() { const app = express() if (PROD) { await setupProd(app, streamContent) } else { await setupDev(app, streamContent) } app.listen(APP_PORT, () => { console.log(`http://localhost:${APP_PORT}`) }) } createServer()
實施這些變更後,您的伺服器將:
- 將 HTML 增量式傳送到瀏覽器,減少首次繪製的時間。
- 無縫處理延遲載入的元件,提高效能和使用者體驗。
伺服器到客戶端數據
在將 HTML 傳送到客戶端之前,您可以完全控制伺服器產生的 HTML。這允許您根據需要添加標籤、樣式、連結或任何其他元素來動態修改結構。
一種特別強大的技術是注入一個<script>標記到 HTML 中。這種方法使您能夠將動態資料直接傳遞給客戶端。 </script>
在此範例中,我們將專注於傳遞環境變量,但您可以傳遞您需要的任何 JavaScript 物件。透過將環境變數傳遞給客戶端,您可以避免在這些變數發生變更時重建整個應用程式。在底部連結的範例儲存庫中,您還可以看到設定檔資料是如何動態傳遞的。
在伺服器上傳遞數據
定義 API_URL
在伺服器上設定 API_URL 環境變數。預設情況下,這將指向 jsonplaceholder。 __INITIAL_DATA__ 將充當全域視窗物件上儲存資料的鍵。
// ./src/entry-server.tsx import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server' import App from './App' export function render(options?: RenderToPipeableStreamOptions) { return renderToPipeableStream(<App />, options) }
將初始資料注入 HTML
建立一個實用函數,在將初始資料傳送到客戶端之前將其註入到 HTML 字串中。此數據將包括 API_URL 等環境變數。
// ./src/Card.tsx import { useState } from 'react' function Card() { const [count, setCount] = useState(0) return ( <div className="card"> <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> ) } export default Card
更新串流內容
使用applyInitialData函數將初始資料注入到HTML中並傳送給客戶端。
// ./src/App.tsx import { lazy, Suspense } from 'react' import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import './App.css' const Card = lazy(() => import('./Card')) function App() { return ( <> <div> <a href="https://vite.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <Suspense fallback='Loading...'> <Card /> </Suspense> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> ) } export default App
處理客戶端上的環境變數
擴展全域視窗類型
更新全域類型宣告以包含 __INITIAL_DATA__ 鍵及其結構。
// ./server/constants.ts export const ABORT_DELAY = 5000
從視窗物件存取 API_URL
// ./server/streamContent.ts import { Transform } from 'node:stream' import { Request, Response, NextFunction } from 'express' import { ABORT_DELAY, HTML_KEY } from './constants' import type { render } from '../src/entry-server' export type StreamContentArgs = { render: typeof render html: string req: Request res: Response next: NextFunction } export function streamContent({ render, html, res }: StreamContentArgs) { let renderFailed = false // Initiates the streaming process by calling the render function const { pipe, abort } = render({ // Handles errors that occur before the shell is ready onShellError() { res.status(500).set({ 'Content-Type': 'text/html' }).send('<pre class="brush:php;toolbar:false">Something went wrong') }, // Called when the shell (initial HTML) is ready for streaming onShellReady() { res.status(renderFailed ? 500 : 200).set({ 'Content-Type': 'text/html' }) // Split the HTML into two parts using the placeholder const [htmlStart, htmlEnd] = html.split(HTML_KEY) // Write the starting part of the HTML to the response res.write(htmlStart) // Create a transform stream to handle the chunks of HTML from the renderer const transformStream = new Transform({ transform(chunk, encoding, callback) { // Write each chunk to the response res.write(chunk, encoding) callback() }, }) // When the streaming is finished, write the closing part of the HTML transformStream.on('finish', () => { res.end(htmlEnd) }) // Pipe the render output through the transform stream pipe(transformStream) }, onError(error) { // Logs errors encountered during rendering renderFailed = true console.error((error as Error).stack) }, }) // Abort the rendering process after a delay to avoid hanging requests setTimeout(abort, ABORT_DELAY) }
使用動態 API_URL 發出請求
// ./server/dev.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import { StreamContentArgs } from './streamContent' const HTML_PATH = path.resolve(process.cwd(), 'index.html') const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx') // Add to args the streamContent callback export async function setupDev(app: Application, streamContent: (args: StreamContentArgs) => void) { const vite = await ( await import('vite') ).createServer({ root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', }) app.use(vite.middlewares) app.get('*', async (req, res, next) => { try { let html = fs.readFileSync(HTML_PATH, 'utf-8') html = await vite.transformIndexHtml(req.originalUrl, html) const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH) // Use the same callback for production and development process streamContent({ render, html, req, res, next }) } catch (e) { vite.ssrFixStacktrace(e as Error) console.error((e as Error).stack) next(e) } }) }
現在,您的客戶端程式碼中可以使用動態環境變量,使您能夠管理伺服器到客戶端的數據,而無需重建 JavaScript 套件。這種方法簡化了配置,並使您的應用程式更加靈活和可擴展。
水分問題
現在您可以將資料從伺服器傳遞到客戶端,如果您嘗試直接在元件內使用此數據,則可能會遇到水合問題。發生這些錯誤是因為伺服器渲染的 HTML 與客戶端上的初始 React 渲染不符。
範例場景
考慮在元件中使用 API_URL 作為簡單字串
// ./src/entry-server.tsx import { renderToPipeableStream, RenderToPipeableStreamOptions } from 'react-dom/server' import App from './App' export function render(options?: RenderToPipeableStreamOptions) { return renderToPipeableStream(<App />, options) }
在這種情況下,伺服器會將 API_URL 的元件渲染為空字串,但在用戶端上,API_URL 已經具有來自 window 物件的值。這種不匹配會導致水合錯誤,因為 React 偵測到伺服器渲染的 HTML 和用戶端的 React 樹之間存在差異。
雖然使用者可能會看到內容快速更新,但 React 在控制台中記錄了水合警告。要解決此問題,您需要確保伺服器和用戶端呈現相同的初始 HTML 或將 API_URL 明確傳遞到伺服器入口點。
解決水合作用問題
要解決錯誤,請透過伺服器入口點將initialData傳遞給App元件。
更新串流內容
// ./src/Card.tsx import { useState } from 'react' function Card() { const [count, setCount] = useState(0) return ( <div className="card"> <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> ) } export default Card
在渲染函數中處理數據
// ./src/App.tsx import { lazy, Suspense } from 'react' import reactLogo from './assets/react.svg' import viteLogo from '/vite.svg' import './App.css' const Card = lazy(() => import('./Card')) function App() { return ( <> <div> <a href="https://vite.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://react.dev" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <Suspense fallback='Loading...'> <Card /> </Suspense> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> </> ) } export default App
在應用程式元件中使用initialData
// ./server/constants.ts export const ABORT_DELAY = 5000
現在,您的伺服器渲染的 HTML 將與客戶端上的初始 React 渲染相匹配,從而消除水合錯誤。 React 將正確協調伺服器和客戶端樹,確保無縫體驗。
對於 API_URL 這樣的動態數據,請考慮使用 React Context 來管理和在伺服器和用戶端之間傳遞預設值。這種方法簡化了跨元件共享資料的管理。您可以在底部的連結儲存庫中找到範例實作。
結論
在本文中,我們探索了 React 的高級 SSR 技術,重點關注實現串流、管理伺服器到客戶端的資料以及解決水合問題。這些方法可確保您的應用程式具有可擴展性、高效能並創造無縫的使用者體驗。
探索程式碼
- 範例:react-ssr-advanced-example
- 模板:react-ssr-streaming-template
- Vite 額外範本: template-ssr-react-streaming-ts
相關文章
這是我的 React SSR 系列的一部分。更多文章敬請期待!
- 建構生產就緒的 SSR React 應用程式
- 使用串流和動態資料的進階 React SSR 技術(你在這裡)
- 在 SSR React 應用程式中設定主題(即將推出)
保持聯繫
我總是樂於接受回饋、合作或討論技術想法 - 請隨時與我們聯繫!
- 投資組合:maxh1t.xyz
- 電子郵件:m4xh17@gmail.com
以上是具有串流媒體和動態資料的高級 React SSR 技術的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

Python和JavaScript在開發環境上的選擇都很重要。 1)Python的開發環境包括PyCharm、JupyterNotebook和Anaconda,適合數據科學和快速原型開發。 2)JavaScript的開發環境包括Node.js、VSCode和Webpack,適用於前端和後端開發。根據項目需求選擇合適的工具可以提高開發效率和項目成功率。

C和C 在JavaScript引擎中扮演了至关重要的角色,主要用于实现解释器和JIT编译器。1)C 用于解析JavaScript源码并生成抽象语法树。2)C 负责生成和执行字节码。3)C 实现JIT编译器,在运行时优化和编译热点代码,显著提高JavaScript的执行效率。
