React SSR - 写个 Demo 一学就会
React SSR - 写个 Demo 一学就会
今天写个小 Demo 来从头实现一下 react 的 SSR,帮助理解 SSR 是如何实现的,有什么细节。
什么是 SSR
SSR 即 Server Side Rendering 服务端渲染,是指将网页内容在服务器端中生成并发送到浏览器的技术。相比于客户端渲染(CSR),SSR 一般用于以下场景:
SEO(搜索引擎优化):由于部分搜索引擎对CSR内容支持不佳,所以SSR可以提升网站在搜索引擎结果中的排名。- 首屏加载速度:由于
SSR可以在服务器端生成完整的HTML页面,用户打开网页时能够更快地看到内容,不会看到长时间的白屏,可以提升用户体验。 - 隐藏某些数据:由于
CSR需要从服务器将数据下载下来进行动态渲染,所以一些数据很容易被他人获取,而SSR由于数据到渲染的过程在服务端实现,所以可以用来隐藏一些不想让他人轻易获得的数据。
如何实现
简单的 SSR 其实实现很简单,只需要在服务端导入要渲染的组件,然后调用 react-dom/server 包中提供的 renderToString 方法将该组件的渲染内容输出为字符串后返回客户端即可。
Server 端的组件
下面写一个简单的例子:
服务端代码:
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from '../ui/App';
const app = express();
app.get('/', (_: unknown, res: express.Response) => {
res.send(renderToString(<App />));
});
app.listen(4000, () => {
console.log('Listening on port 4000');
});
此处要注意服务端需要支持 jsx 语法的解析,我这里直接使用 esno 执行 ts 代码,在 tsconfig.json 中配置 jsx 即可。
其实看到这里就能明白为什么在 SSR 的页面上使用 window、localstorage 等浏览器 API 需要放到 useEffect 里了,因为该页面的组件都会被 server 端读取解析,而 server 端并没有这些 API。
然后看下 App 组件的代码:
import React, { useCallback } from 'react';
export default () => {
const log = useCallback(() => {
console.log('Hello world');
}, []);
return (
<div>
<p>react ssr demo</p>
<button onClick={log}>Click me</button>
</div>
);
};
启动服务器后 server 端就会使用 renderToString 将 <App /> 渲染成 html 字符串,然后通过 send 返回给前端,下面就是服务端返回的 html 内容:
<div>
<p>react ssr demo</p>
<button>Click me</button>
</div>
打开浏览器访问该地址即可看到服务端返回了该 html 片段:

hydrate 复活组件
如果你跟着上面的操作很快就会发现问题:为什么点按钮没法操作了?
其实原因很简单,因为我们只拿到了一个 html 并没有任何的 js,事件绑定等自然是无法实现的,要复活组件的交互我们还需要很重要的一步 - hydrate 也就是常说的水合。
hydrate 即通过 react 将对应的组件重新渲染到 SSR 渲染的静态内容上,类似于 render 差异点在于 render 会忽略 root 元素中现有的 dom 而 hydrate 则会复用并会进行内容匹配检查。
Hydration failed because the initial UI does not match what was rendered on the server.
如果遇到上述错误即表示在客户端执行 hydrate 时服务端返回的初始的 dom 和 hydrate 接收到的需要进行渲染的 dom 不匹配。
说了这么多我们再来看下代码如何编写,首先要进行 hydrate 我们需要客户端的代码来执行:
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root')!, <App />);
然后将该代码进行编译打包,我这里就直接使用 webpack 进行打包:
const path = require('path');
module.exports = {
entry: './ui/index.tsx',
output: {
path: path.resolve(__dirname, 'static'),
filename: 'bundle.js'
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
module: {
rules: [
{
test: /\.(t|j)sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-react', '@babel/preset-typescript']
}
}
}
]
}
};
打包完成后生成一个 bundle.js 即可在客户端使用它来进行 hydrate。
然后我们再修改下 server 端的代码:
app.get('/', (_: unknown, res: express.Response) => {
res.send(
`
<div id="root">${renderToString(<App />)}</div>
<script src="/bundle.js"></script>
`
);
});
app.use(express.static('static'));
我们在静态内容的外层套上 root 元素,然后在下方引入我们刚刚编译的脚本,然后就可以在客户端看到我们想要的结果:

可以看到事件可以正常触发了。
此处还有个注意点,在 server 端要注意将静态字符串包裹在 root 元素中不要添加换行空格等,不然 react 在 hydrate 时依旧会因为内容不匹配而提示 Hydration failed(仅在 hydrateRoot 时出现,如果使用 hydrate 不会报错,不过 18 中 hydrate 已经被弃用。)
动态数据
此时有些同学可能发现一些问题:前面的内容所渲染的内容都是静态的,如果要针对用户渲染出不同的内容比如用户信息等如何是好?
其实很简单,只需要在服务端将对应的信息作为 props 进行渲染即可,我们下面使用 userName 模拟一下:
app.get('/', (_: unknown, res: express.Response) => {
const userName = ['张三', '李四', '王五', '赵六'][(Math.random() * 4) | 0];
res.send(
`
<div id="root">${renderToString(<App userName={userName} />)}</div>
<script src="/bundle.js"></script>
`
);
});
可是客户端要如何与服务端匹配呢?此处有两种解决方案:
- 客户端获取对应的信息并在信息获取完成后再进行
hydrate操作。 - 服务端将获取到的信息放在页面中。
可以看出方案 1 会带来明显的延时,所以一般会采用方案 2,实现一般可以使用全局变量或特定标签来实现:
app.get('/', (_: unknown, res: express.Response) => {
const userName = ['张三', '李四', '王五', '赵六'][(Math.random() * 4) | 0];
res.send(
`
<div id="root">${renderToString(<App userName={userName} />)}</div>
<script>
window.__initialState = { userName: '${userName}' };
</script>
<script src="/bundle.js"></script>
`
);
});
import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import App from './App';
hydrateRoot(document.getElementById('root')!, <App {...window.__initialState} />);
总结
React中的SSR可以通过renderToString来实现,但是只能输出静态内容,要让页面支持交互需要搭配hydrate使用。- 实现
SSR时服务端需要支持jsx语法的解析,因为服务端也需要读取组件。 hydrate会检查服务端与客户端的内容是否匹配。- 要实现动态数据需要在客户端与服务端之间做好如何使用初始
props的约定。
最后
本文的 demo 代码放置在 React SSR Demo 中,可自行取阅。
React SSR - 写个 Demo 一学就会的更多相关文章
- 打算写一个《重学Node.js》系列,希望大家多多支持
先放上链接吧,项目已经开始2周了:https://github.com/hellozhangran/happy-egg-server 想法 现在是2019年11月24日,还有人要开始学习Node.js ...
- 放弃antd table,基于React手写一个虚拟滚动的表格
缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...
- 序(转) · 为 React 而写 -- 废话比较多, 你也可以说丰满
流形 2 年前 (废话比较多 从今年开始,就一直在规划技术沉淀这事. 在阿里巴巴工作的这些年,前端团队日益壮大,同时聚集了一帮志趣相投的伙伴. 作为团队负责人,一方面是借团队在技术道路上的 ...
- 手写Spring+demo+思路
我在学习Spring的时候,感觉Spring是很难的,通过学习后,发现Spring没有那么难,只有你去学习了,你才会发现,你才会进步 1.手写Spring思路: 分为配置.初始化.运行三个阶段如下图 ...
- 如何写好demo——学习感悟
文章标题:教你如何写好Demo应用 如何制作出最有用的demo呢? 简,易 在demo中,我们要专注于单一的主题.我们的教学覆盖了很大的知识范围,因此,化整为零是非常必要的. 例如,我们要说明Andr ...
- React SSR in Action
React SSR in Action react render HTML string from the server ReactDOMServer https://reactjs.org/docs ...
- React 学习资源分享 菜鸟刚学5天 博客写的不多 不懂写博客的套路
http://www.ruanyifeng.com/blog/2015/03/react.html 首先个人强烈推荐 阮一峰的React基础 细细过一遍,看得出大师的用心良苦 然后就开始看书般的过ht ...
- 写简单游戏,学编程语言-python篇
好吧, 首先得承认这个题目写的夸大了,人才菜鸟一枚,游戏相关编程也是知道点概念.但是本人对游戏开发比较感兴趣,相信大多数喜欢玩玩游戏,因为它给人确实带来很多乐趣,而编程语言的学习最少对于我来说比较乏味 ...
- 阿里react整合库dva demo分析
p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 24.0px "Helvetica Neue"; color: #404040 } p. ...
- Python flask+react+antd实现登陆demo
这两天在研究flask和antd,想把这俩个东西结合来使用,单独学antd的时候用的是dva来配置,但是发现这样与flask结合的话需要启动两个服务,作为flask只是作为数据的接口,并没用用到其强大 ...
随机推荐
- 手把手带你从0完成医疗行业影像图像检测三大经典模型InceptionV3-RestNet50-VGG16(附python源代码及数据库)——改变世界经典人工智能项目实战(一)手把手教学迁移学习
目录 1.迁移学习简介 2.项目简介 3.糖尿病视网膜病变数据集 4.考虑类别不平衡问题 5.定义模型质量 6.定义损失函数 7.预处理图像 8.搭建迁移学习网络 VGG16 迁移学习网络 Incep ...
- 逍遥自在学C语言 | 第一个C语言程序 九层之台起于垒土
一.人物简介 第一位闪亮登场,有请今后会一直教我们C语言的老师 -- 自在. 第二位上场的是和我们一起学习的小白程序猿 -- 逍遥. 二.C语言简介 C语言是一种高级语言,运行效率仅次于汇编,支持跨平 ...
- abp(net core)+easyui+efcore实现仓储管理系统——ABP升级7.3下(五十九)
Abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统--ABP总体介绍(一) abp(net core)+ ...
- 最新centos7 部署 k8s v1.26,简单易懂,跟着命令敲就完事
其实没什么好说的,搭环境搞了一整天,人已经麻了,踩了很多坑,网上教程的版本大都比较旧,总是和最新版本各种地方不兼容,把坑踩完了,k8s目前最新的版本是v1.26,跟着命令敲就行了,我已经重复部署了很多 ...
- python入门教程之九日期时间常用操作
Python 提供了一个 time 和 calendar 模块可以用于格式化日期和时间. 时间间隔是以秒为单位的浮点小数. 每个时间戳都以自从1970年1月1日午夜(历元)经过了多长时间来表示. Py ...
- [Java EE]缓存技术初探
1 背景 使用场景:计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存. 高并发下,为提高 频繁 查询 大量 可能常用的 数据库数据的 查询效率. 大部分情况下, ...
- FTP FileZilla 425 Can't open data connection for transfer of "/" 错误: 读取目录列表失败
如图所示: 在谷歌百度搜了很多资料都没有解决,主动被动模式端口入站规则什么能设置的都设置了结果还是不行,尝试换了一个软件用了FTP Rush就直接可以连上了. 具体原因有空再查找吧,目前问题算是解决了 ...
- PHP__采集类__Snoopy
Snoopy 目录 了解Snoopy.1 功能:...1 下载Snoopy:...2 Snoopy常用 ...
- 区块链——Lab2
区块链的典型数据结构 比特币:UTXO模型,以交易后找零为中心 ETH:Account 模型,以账户余额为中心(就是账户的形式) 区块链交易 用户发起交易 矿工验证交易(能够得到 区块奖励) 验证成功 ...
- Analysis of Variance ANOVA versus T test 方差分析和T检验
Levels are different groupings within the same independent variable(factor). Eg. if the independent ...