React SSR - 写个 Demo 一学就会

今天写个小 Demo 来从头实现一下 reactSSR,帮助理解 SSR 是如何实现的,有什么细节。

什么是 SSR

SSRServer Side Rendering 服务端渲染,是指将网页内容在服务器端中生成并发送到浏览器的技术。相比于客户端渲染(CSR),SSR 一般用于以下场景:

  1. SEO (搜索引擎优化):由于部分搜索引擎对 CSR 内容支持不佳,所以 SSR 可以提升网站在搜索引擎结果中的排名。
  2. 首屏加载速度:由于 SSR 可以在服务器端生成完整的 HTML 页面,用户打开网页时能够更快地看到内容,不会看到长时间的白屏,可以提升用户体验。
  3. 隐藏某些数据:由于 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 的页面上使用 windowlocalstorage 等浏览器 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 元素中现有的 domhydrate 则会复用并会进行内容匹配检查。

Hydration failed because the initial UI does not match what was rendered on the server.

如果遇到上述错误即表示在客户端执行 hydrate 时服务端返回的初始的 domhydrate 接收到的需要进行渲染的 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 元素中不要添加换行空格等,不然 reacthydrate 时依旧会因为内容不匹配而提示 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>
`
);
});

可是客户端要如何与服务端匹配呢?此处有两种解决方案:

  1. 客户端获取对应的信息并在信息获取完成后再进行 hydrate 操作。
  2. 服务端将获取到的信息放在页面中。

可以看出方案 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} />);

总结

  1. React 中的 SSR 可以通过 renderToString 来实现,但是只能输出静态内容,要让页面支持交互需要搭配 hydrate 使用。
  2. 实现 SSR 时服务端需要支持 jsx 语法的解析,因为服务端也需要读取组件。
  3. hydrate 会检查服务端与客户端的内容是否匹配。
  4. 要实现动态数据需要在客户端与服务端之间做好如何使用初始 props 的约定。

最后

本文的 demo 代码放置在 React SSR Demo 中,可自行取阅。

React SSR - 写个 Demo 一学就会的更多相关文章

  1. 打算写一个《重学Node.js》系列,希望大家多多支持

    先放上链接吧,项目已经开始2周了:https://github.com/hellozhangran/happy-egg-server 想法 现在是2019年11月24日,还有人要开始学习Node.js ...

  2. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  3. 序(转) · 为 React 而写 -- 废话比较多, 你也可以说丰满

    流形 2 年前 (废话比较多       从今年开始,就一直在规划技术沉淀这事. 在阿里巴巴工作的这些年,前端团队日益壮大,同时聚集了一帮志趣相投的伙伴. 作为团队负责人,一方面是借团队在技术道路上的 ...

  4. 手写Spring+demo+思路

    我在学习Spring的时候,感觉Spring是很难的,通过学习后,发现Spring没有那么难,只有你去学习了,你才会发现,你才会进步 1.手写Spring思路: 分为配置.初始化.运行三个阶段如下图 ...

  5. 如何写好demo——学习感悟

    文章标题:教你如何写好Demo应用 如何制作出最有用的demo呢? 简,易 在demo中,我们要专注于单一的主题.我们的教学覆盖了很大的知识范围,因此,化整为零是非常必要的. 例如,我们要说明Andr ...

  6. React SSR in Action

    React SSR in Action react render HTML string from the server ReactDOMServer https://reactjs.org/docs ...

  7. React 学习资源分享 菜鸟刚学5天 博客写的不多 不懂写博客的套路

    http://www.ruanyifeng.com/blog/2015/03/react.html 首先个人强烈推荐 阮一峰的React基础 细细过一遍,看得出大师的用心良苦 然后就开始看书般的过ht ...

  8. 写简单游戏,学编程语言-python篇

    好吧, 首先得承认这个题目写的夸大了,人才菜鸟一枚,游戏相关编程也是知道点概念.但是本人对游戏开发比较感兴趣,相信大多数喜欢玩玩游戏,因为它给人确实带来很多乐趣,而编程语言的学习最少对于我来说比较乏味 ...

  9. 阿里react整合库dva demo分析

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 24.0px "Helvetica Neue"; color: #404040 } p. ...

  10. Python flask+react+antd实现登陆demo

    这两天在研究flask和antd,想把这俩个东西结合来使用,单独学antd的时候用的是dva来配置,但是发现这样与flask结合的话需要启动两个服务,作为flask只是作为数据的接口,并没用用到其强大 ...

随机推荐

  1. GIT 操作大全 基于廖雪峰

    命令显示从最近到最远的提交日志  git log /  git log --pretty=oneline 回退到上一个版本:$ git reset --hard HEAD^   (用HEAD表示当前版 ...

  2. OSM(OpenStreetMap)全球路网数据下载方式介绍

      本文对OpenStreetMap(OSM)网页与各类OSM数据的多种下载方式加以详细介绍,并对不同数据下载方式加以对比.   OSM数据包含道路与铁路路网.建筑.水体.土地利用.兴趣点.行政区边界 ...

  3. php对接snmp设备详细讲解

    1.Php安装snmp扩展 1.基础环境准备 Php7.2版本 yum -y install php72w-snmp Php7.4版本 yum install net-snmp php-snmp ne ...

  4. 【Spring注解驱动】(三)Servlet 3.0

    前言 今天是7.21日,终于是看完了..暑假在家学习是真的差点意思 1 Servlet 3.0简介 Servlet 2.0是在web.xml中配置servlet filter.listener.Dis ...

  5. 迁移学习(COAL)《Generalized Domain Adaptation with Covariate and Label Shift CO-ALignment》

    论文信息 论文标题:Generalized Domain Adaptation with Covariate and Label Shift CO-ALignment论文作者:Shuhan Tan, ...

  6. ray-分布式计算框架-集群与异步Job管理

    0. ray 简介 ray是开源分布式计算框架,为并行处理提供计算层,用于扩展AI与Python应用程序,是ML工作负载统一工具包 Ray AI Runtime ML应用程序库集 Ray Core 通 ...

  7. TEMP_FAILURE_RETRY宏的用法

    #define TEMP_FAILURE_RETRY(expression) \     (__extension__\      ({ long int __result;\          do ...

  8. 揭开神秘面纱,会stream流就会大数据

    目录 准备工作 1.map类 1.1 java stream map 1.2 spark map 1.2.1 MapFunction 1.2.2 MapPartitionsFunction 2.fla ...

  9. h5新增特性 和 css3 新特性

    H5新增: 1)用于绘画 canvas 元素. 2) 用于媒介回放的 video 和 audio 元素. 3)语义化标签 article.footer.header.nav.section3) 4)表 ...

  10. ES6 新增的一些特性

    还有symbol和set,map, bind,call,apply 1. let关键字 (1)基本用法:let关键字用来声明变量,它的用法类似于var,都是用来声明变量. (2)块级作用域:let声明 ...