• 简体中文
  • SSR 与水合

    服务端渲染(SSR)

    服务端渲染(SSR)是一种技术:它帮我们在服务端把组件渲染成 HTML 字符串,把这些字符串直接发送到浏览器,最后再在客户端把静态标记“水合”成一个完全可交互的应用。

    React

    假设我们想用 React 渲染一个无状态应用。为此,我们需要使用 express、react 和 react-dom/server。因为这是一个无状态应用,所以不需要 react-dom/client。

    我们来看看具体怎么做:

    • express 帮我们构建一个可以用 Node 运行的 Web 应用,
    • react 帮我们构建应用中使用的 UI 组件,
    • react-dom/server 帮我们在服务端渲染组件。
    // tsconfig.json
    {
      "compilerOptions": {
        "noImplicitAny": false,
        "noEmitOnError": true,
        "removeComments": false,
        "sourceMap": true,
        "target": "esnext"
      },
      "include": ["**/*"]
    }

    注意: 不要忘记删除 tsconfig.json 文件中的所有注释。

    // app.tsx
    export const App = () => {
      return (
        <html>
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title>Static Server-side-rendered App</title>
          </head>
          <body>
            <div>Hello World!</div>
          </body>
        </html>
      )
    }
    // server.tsx
    import express from 'express'
    import React from 'react'
    import ReactDOMServer from 'react-dom/server'
    
    import { App } from './app.tsx'
    
    const port = Number.parseInt(process.env.PORT || '3000', 10)
    const app = express()
    
    app.get('/', (_, res) => {
      const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
        onShellReady() {
          res.setHeader('content-type', 'text/html')
          pipe(res)
        },
      })
    })
    
    app.listen(port, () => {
      console.log(`Server is listening at ${port}`)
    })
    tsc --build
    node server.js

    水合

    水合会把来自服务端的初始 HTML 快照变成一个在浏览器中运行的、完全可交互的应用。对组件进行“水合”的正确方式是使用 hydrateRoot。

    React

    假设我们想用 React 渲染一个有状态应用。为此,我们需要使用 express、react、react-dom/server 和 react-dom/client。

    我们来看看具体怎么做:

    • express 帮我们构建一个可以用 Node 运行的 Web 应用,
    • react 帮我们构建应用中使用的 UI 组件,
    • react-dom/server 帮我们在服务端渲染组件,
    • react-dom/client 帮我们在客户端水合组件。

    注意: 不要忘记,即便我们可以在服务端渲染组件,也必须在客户端对它们进行“水合”,才能让它们具备交互性。

    // tsconfig.json
    {
      "compilerOptions": {
        "noImplicitAny": false,
        "noEmitOnError": true,
        "removeComments": false,
        "sourceMap": true,
        "target": "esnext"
      },
      "include": ["**/*"]
    }

    注意: 不要忘记删除 tsconfig.json 文件中的所有注释。

    // app.tsx
    export const App = () => {
      return (
        <html>
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title>Static Server-side-rendered App</title>
          </head>
          <body>
            <div>Hello World!</div>
          </body>
        </html>
      )
    }
    // main.tsx
    import ReactDOMClient from 'react-dom/client'
    
    import { App } from './app.tsx'
    
    ReactDOMClient.hydrateRoot(document, <App />)
    // server.tsx
    import express from 'express'
    import React from 'react'
    import ReactDOMServer from 'react-dom/server'
    
    import { App } from './app.tsx'
    
    const port = Number.parseInt(process.env.PORT || '3000', 10)
    const app = express()
    
    app.use('/', (_, res) => {
      const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
        bootstrapScripts: ['/main.js'],
        onShellReady() {
          res.setHeader('content-type', 'text/html')
          pipe(res)
        },
      })
    })
    
    app.listen(port, () => {
      console.log(`Server is listening at ${port}`)
    })
    tsc --build
    node server.js

    警告: 传给 hydrateRoot 的 React 树需要产生与服务端相同的输出。 导致水合错误的最常见原因包括:

    • 根节点内由 React 生成的 HTML 周围存在多余空白(比如换行)。
    • 在渲染逻辑中使用 typeof window !== 'undefined' 这类判断。
    • 在渲染逻辑中使用 window.matchMedia 这类仅浏览器可用的 API。
    • 在服务端和客户端渲染不同的数据。

    React 能从某些水合错误中恢复,但你必须像修复其他 bug 一样修复它们。最好的情况下,它们会导致性能下降;最坏的情况下,事件处理器可能被挂到错误的元素上。

    关于注意事项和常见陷阱,可以在这里了解更多:hydrateRoot