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!'))
配置 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()
app.use(express.static('public'))
app.get('/', (req, res) => {
const content = renderToString(<Home/>)
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, () => console.log('Example app listening on port 3000!'))
实现同构
服务端只能渲染 HTML 结构,事件绑定需要在客户端完成。创建 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'))
配置路由
定义路由信息:
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)
工作原理
- Node 服务器接收请求,获取当前 URL 路径
- 在路由表中查找对应组件,获取需要的数据
- 将数据通过 props、context 或 store 传入组件
- 使用
renderToString()将组件渲染为 HTML 字符串 - 将数据注入到 HTML 中,返回给浏览器
- 浏览器接收 HTML 并开始渲染
- 客户端 JavaScript 执行,完成事件绑定和交互(hydrate)
- 浏览器复用服务端输出的 HTML 节点,完成整个流程
关键点
- SSR 的核心是使用
renderToString在服务端将 React 组件转换为 HTML 字符串 - 同构是指同一套代码在服务端和客户端各执行一遍,服务端负责结构渲染,客户端负责事件绑定
- 服务端路由使用
StaticRouter,客户端路由使用BrowserRouter - 客户端使用
ReactDOM.hydrate而非render,以复用服务端渲染的 DOM 节点 - 需要配置两套 Webpack,分别打包服务端和客户端代码
目录