React SSR - 写个 Demo 一学就会

2023-10-29

今天写个小 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 片段:

picture 1

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 元素,然后在下方引入我们刚刚编译的脚本,然后就可以在客户端看到我们想要的结果:

picture 2

可以看到事件可以正常触发了。

此处还有个注意点,在 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 中,可自行取阅。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • QT项目中ui_.h文件详解

    在QT工程中的mainwindow h常见的几行 namespace Ui class MainWindow private Ui MainWindow ui 在QT工程中的mainwindow cpp常见的几行 include ui ma
  • javaweb基础

    静态网站 在tomcat的webapps目录下创建一个目录 命名必须不包含中文和空格 这个目录称之为项目目录 在项目目录下创建一个html文件 动态网站 在tomcat的webapps目录下创建一个项目目录 在项目目录下创建如下内容 WEB
  • sudo执行命令 报Permission denied

    使用sudo su切换到root用户 再运行有关命令即可
  • qt中在不同类间传递参数的几种方式

    最近开发一个程序 需要多个源文件 包括若干个头文件和若干个定义文件 因此如何在多个源程序间开发传递变量就成了一个关键问题 刚开始我通过构造函数传递参数 能成功传递但数据却不会自动更新 随后想到通过全局变量传递参数 一般来说在多个源程序间传递
  • 运维Shell脚本牛刀小试(九): 重定向操作符“>“及双重定向“>>“

    运维Shell脚本小试牛刀 一 运维Shell脚本小试牛刀 二 运维Shell脚本小试牛刀 三 cd dirname 0 pwd 命令详解 运维Shell脚本小试牛刀 四 多层嵌套if elif elif else fi 蜗牛杨哥的博客 C
  • 二叉树的中序遍历、两数之和、整数反转

    Java学习路线 搬砖工逆袭Java架构师 简介 Java领域优质创作者 CSDN哪吒公众号作者 Java架构师奋斗者 扫描主页左侧二维码 加入群聊 一起学习 一起进步 欢迎点赞 收藏 留言 目录 1 LeetCode 94 二叉树的中序遍
  • 项目中使用JsonSerializer将JavaBean中Date毫秒级转秒级的简便方法

    此项目是基于SpringBoot实现的 我的JavaBean中有两个Date属性 如下 private Date createTime private Date updateTime 我通过crud 增删改查 操作返回的是毫秒级别的 但我前
  • Qt中多进程写法

    进程 运行中的程序 比如下面这些 没运行的就不算 线程 简单记为程序中的函数 qt中多进程写法 这个比较简单 就没有什么文字说明了 1 widget h ifndef WIDGET H define WIDGET H include
  • Source insight 4工程设置相对地址

    我是阿荣 关注我 在技术路上一起精进 Source insight 使用说明 Source insight 软件版本 V4 00 0084 建议都使用 V4 0 版本的 Source insight 新建相对地址的 Source insig
  • java.lang.IllegalArgumentException: taglib definition not consistent with specification version

    tomcat8 0中使用taglib 错误表现 java lang IllegalArgumentException taglib definition not consistent with specification version 原
  • 4399游戏校招笔试题

    设一组初始记录关键字序列为 49 38 65 97 76 13 27 49 则以第一个关键字49为基准而得到的一趟快速排序结果是 A 38 13 27 49 49 65 97 76 B 13 27 38 49 65 76 97 49 C 2
  • 常用技术指标之一文读懂KDJ指标

    什么是KDJ指标 KDJ中文名又叫随机指标 英文名叫Stochastic oscillator 由乔治 莱恩 George Lane 于20世纪50年代首创 最早用于期货市场 KDJ指标能比较迅速 直观地研判行情 主要用于分析中短期趋势 是
  • 主键约束(PRIMARY KEY) [MySQL][数据库]

    主键约束 PRIMARY KEY 主键约束的特点 主键约束相当于唯一性约束 非空约束 主键约束不允许重复 也不允许出现空值 一个表最多只能有一个主键约束 建立主键约束可以在列级别创建 也可以再表级别创建 主键约束对应着表中的一列或者多列 对
  • JDK8:使用Optional进行变量判空、集合遍历

    防止 NPE 是程序员的基本修养 NPE Null Pointer Exception 一直是我们最头疼的问题 也是最容易忽视的地方 NPE常是导致Java应用程序失败的最常见的原因 在日常研发工作中 经常会处理各种变量 集合 但在使用的过
  • 【vue、uni-app】文本信息的完全显示(回车换行、连续空格、数字&英文换行)

    vue uni app 文本信息的完全显示 回车换行 连续空格 数字 英文换行 数据说明 完整显示 一 vue端 1 不做处理的效果 2 处理后 二 uni app端 1 不做处理的效果 2 处理后 总结 记录学习的轨迹 2021 12 0
  • 线程知识点补充

    全局解释器锁GIL 是什么 GIL本质也是一把互斥锁 将并发变成串行 降低效率以保证数据的安全性 每有一个进程 进程内就必然有一个用来执行代码的线程 也会有一个用来执行垃圾回收的线程 为了避免执行代码的线程和执行垃圾回收的线程对同一份数据进
  • golang 单元测试、性能测试、性能监控技术

    golang 单元测试 性能测试 性能监控技术 go语言提供了强大的测试工具 下面举例简单介绍一下 go test 单元测试 go test bench 性能测试 go tool pprof 性能监控 go test 单元测试 例如对包he
  • 至强服务器性能排行,英特尔至强处理器排名天梯 至强cpu天梯2020排名

    排名 名称 评分 1 Intel Xeon Platinum 8173M 2 00GHz 28 860 2 Intel Xeon Gold 6154 3 00GHz 27 722 3 Intel Xeon Gold 6138 2 00GHz
  • 【研究生】毕业答辩PPT制作和讲述要点(整理)

    引用网址 http blog sciencenet cn blog 53846 232974 html 引言 在QQ群上和研三的点评答辩ppt制作结果 不知不觉 唠叨 了很多 其中 让大家共享一下彼此的ppt文档 取人所长 不想 学生杨涛有
  • React SSR - 写个 Demo 一学就会

    今天写个小 Demo 来从头实现一下 react 的 SSR 帮助理解 SSR 是如何实现的 有什么细节 什么是 SSR SSR 即 Server Side Rendering 服务端渲染 是指将网页内容在服务器端中生成并发送到浏览器的技术