React 服务端渲染实现

手动搭建 React SSR 框架的完整流程和原理

问题

如何实现 React 服务端渲染(SSR),以及它的工作原理是什么?

解答

什么是 SSR

服务端渲染(Server-Side Rendering,SSR)是指由服务器完成页面 HTML 结构拼接,发送到浏览器后再绑定状态与事件,使页面完全可交互的过程。

SSR 解决两个主要问题:

  • SEO 优化:搜索引擎爬虫可以直接抓取完整渲染的页面
  • 首屏加载:减少白屏时间,提升用户体验

搭建基础服务器

使用 Express 创建服务器,监听请求并返回 HTML:

const express = require('express')
const app = express()

app.get('/', (req, res) => res.send(`
<html>
  <head>
    <title>ssr demo</title>
  </head>
  <body>
    Hello world
  </body>
</html>
`))

app.listen(3000, () => console.log('Example app listening on port 3000!'))

编写 React 组件

创建简单的 React 组件:

import React from 'react'

const Home = () => {
  return <div>home</div>
}

export default Home

配置 Webpack

创建 webpack.server.js 配置文件,让服务器识别 JSX:

const path = require('path')
const nodeExternals = require('webpack-node-externals')

module.exports = {
  target: 'node',
  mode: 'development',
  entry: './app.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  externals: [nodeExternals()],
  module: {
    rules: [{
      test: /\.js?$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
        presets: ['react', 'stage-0', ['env', {
          targets: {
            browsers: ['last 2 versions']
          }
        }]]
      }
    }]
  }
}

服务端渲染组件

使用 renderToString 将 React 组件转换为 HTML 字符串:

import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from './src/containers/Home'

const app = express()
const content = renderToString(<Home/>)

app.get('/', (req, res) => res.send(`
<html>
  <head>
    <title>ssr demo</title>
  </head>
  <body>
    <div id="root">${content}</div>
  </body>
</html>
`))

app.listen(3000)

实现同构

服务端只能渲染 HTML 结构,事件绑定需要在客户端完成。通过引入客户端 JS 实现同构:

import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import Home from './src/containers/Home'

const app = express()
app.use(express.static('public'))

const content = renderToString(<Home/>)

app.get('/', (req, res) => res.send(`
<html>
  <head>
    <title>ssr demo</title>
  </head>
  <body>
    <div id="root">${content}</div>
    <script src="/index.js"></script>
  </body>
</html>
`))

app.listen(3000)

创建 webpack.client.js 配置客户端代码:

const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [{
      test: /\.js?$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
        presets: ['react', 'stage-0', ['env', {
          targets: {
            browsers: ['last 2 versions']
          }
        }]]
      }
    }]
  }
}

客户端入口文件 src/client/index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import Router from '../Routers'

const App = () => {
  return (
    <BrowserRouter>
      {Router}
    </BrowserRouter>
  )
}

ReactDOM.hydrate(<App/>, document.getElementById('root'))

配置路由

创建路由配置 Routers.js

import React from 'react'
import { Route } from 'react-router-dom'
import Home from './containers/Home'

export default (
  <div>
    <Route path="/" exact component={Home}></Route>
  </div>
)

服务端使用 StaticRouter 渲染路由:

import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import Router from '../Routers'

const app = express()
app.use(express.static('public'))

app.get('*', (req, res) => {
  const content = renderToString(
    <StaticRouter location={req.path} context={{}}>
      {Router}
    </StaticRouter>
  )
  
  res.send(`
    <html>
      <head>
        <title>ssr demo</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `)
})

app.listen(3000)

工作原理

React SSR 的完整流程:

  1. Node 服务器接收请求,获取当前 URL 路径
  2. 在路由表中查找对应组件,获取需要的数据
  3. 将数据通过 props、context 或 store 传入组件
  4. 使用 renderToString() 将组件渲染为 HTML 字符串
  5. 将数据注入到 HTML 中,一并返回给浏览器
  6. 浏览器接收 HTML 并渲染,加载客户端 JS
  7. 客户端 JS 执行,进行节点对比(hydrate)
  8. 绑定事件和交互逻辑,复用服务端输出的 HTML 节点

关键点

  • SSR 通过 renderToString 在服务端生成 HTML,客户端使用 hydrate 复用节点并绑定事件
  • 同构是指同一套 React 代码在服务端和客户端各执行一遍,服务端负责结构,客户端负责交互
  • 服务端使用 StaticRouter,客户端使用 BrowserRouter 来处理路由
  • 需要配置两套 Webpack,分别打包服务端和客户端代码
  • 成熟方案可以直接使用 Next.js 框架