首頁 > web前端 > js教程 > 具有串流媒體和動態資料的高級 React SSR 技術

具有串流媒體和動態資料的高級 React SSR 技術

Susan Sarandon
發布: 2025-01-05 03:49:38
原創
230 人瀏覽過

Advanced React SSR Techniques with Streaming and Dynamic Data

隨著應用程式的成長,挑戰也會隨之增加。為了保持領先地位,掌握先進的 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中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板