搜索
首页web前端js教程具有流媒体和动态数据的高级 React SSR 技术

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></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="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1 id="Vite-React">Vite + React</h1>
      <suspense fallback="Loading...">
        <card></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></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="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1 id="Vite-React">Vite + React</h1>
      <suspense fallback="Loading...">
        <card></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></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="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1 id="Vite-React">Vite + React</h1>
      <suspense fallback="Loading...">
        <card></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中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
在JavaScript中替换字符串字符在JavaScript中替换字符串字符Mar 11, 2025 am 12:07 AM

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

构建您自己的Ajax Web应用程序构建您自己的Ajax Web应用程序Mar 09, 2025 am 12:11 AM

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

如何创建和发布自己的JavaScript库?如何创建和发布自己的JavaScript库?Mar 18, 2025 pm 03:12 PM

文章讨论了创建,发布和维护JavaScript库,专注于计划,开发,测试,文档和促销策略。

如何在浏览器中优化JavaScript代码以进行性能?如何在浏览器中优化JavaScript代码以进行性能?Mar 18, 2025 pm 03:14 PM

本文讨论了在浏览器中优化JavaScript性能的策略,重点是减少执行时间并最大程度地减少对页面负载速度的影响。

如何使用浏览器开发人员工具有效调试JavaScript代码?如何使用浏览器开发人员工具有效调试JavaScript代码?Mar 18, 2025 pm 03:16 PM

本文讨论了使用浏览器开发人员工具的有效JavaScript调试,专注于设置断点,使用控制台和分析性能。

jQuery矩阵效果jQuery矩阵效果Mar 10, 2025 am 12:52 AM

将矩阵电影特效带入你的网页!这是一个基于著名电影《黑客帝国》的酷炫jQuery插件。该插件模拟了电影中经典的绿色字符特效,只需选择一张图片,插件就会将其转换为充满数字字符的矩阵风格画面。快来试试吧,非常有趣! 工作原理 插件将图片加载到画布上,读取像素和颜色值: data = ctx.getImageData(x, y, settings.grainSize, settings.grainSize).data 插件巧妙地读取图片的矩形区域,并利用jQuery计算每个区域的平均颜色。然后,使用

如何构建简单的jQuery滑块如何构建简单的jQuery滑块Mar 11, 2025 am 12:19 AM

本文将引导您使用jQuery库创建一个简单的图片轮播。我们将使用bxSlider库,它基于jQuery构建,并提供许多配置选项来设置轮播。 如今,图片轮播已成为网站必备功能——一图胜千言! 决定使用图片轮播后,下一个问题是如何创建它。首先,您需要收集高质量、高分辨率的图片。 接下来,您需要使用HTML和一些JavaScript代码来创建图片轮播。网络上有很多库可以帮助您以不同的方式创建轮播。我们将使用开源的bxSlider库。 bxSlider库支持响应式设计,因此使用此库构建的轮播可以适应任何

如何使用Angular上传和下载CSV文件如何使用Angular上传和下载CSV文件Mar 10, 2025 am 01:01 AM

数据集对于构建API模型和各种业务流程至关重要。这就是为什么导入和导出CSV是经常需要的功能。在本教程中,您将学习如何在Angular中下载和导入CSV文件

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

VSCode Windows 64位 下载

VSCode Windows 64位 下载

微软推出的免费、功能强大的一款IDE编辑器

WebStorm Mac版

WebStorm Mac版

好用的JavaScript开发工具

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

Atom编辑器mac版下载

Atom编辑器mac版下载

最流行的的开源编辑器