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 的完整流程:
- Node 服务器接收请求,获取当前 URL 路径
- 在路由表中查找对应组件,获取需要的数据
- 将数据通过 props、context 或 store 传入组件
- 使用
renderToString()将组件渲染为 HTML 字符串 - 将数据注入到 HTML 中,一并返回给浏览器
- 浏览器接收 HTML 并渲染,加载客户端 JS
- 客户端 JS 执行,进行节点对比(hydrate)
- 绑定事件和交互逻辑,复用服务端输出的 HTML 节点
关键点
- SSR 通过
renderToString在服务端生成 HTML,客户端使用hydrate复用节点并绑定事件 - 同构是指同一套 React 代码在服务端和客户端各执行一遍,服务端负责结构,客户端负责交互
- 服务端使用
StaticRouter,客户端使用BrowserRouter来处理路由 - 需要配置两套 Webpack,分别打包服务端和客户端代码
- 成熟方案可以直接使用 Next.js 框架
目录